Skip to content

Commit

Permalink
fix(loyalty points): Add points after some days gap with cron to prev…
Browse files Browse the repository at this point in the history
…ent fast cancellations (#250)

Updating with ecomplus/app-loyalty-points@8942e05

* fix(loyalty points): Updating with https://github.com/ecomplus/app-loyalty-points

* chore(pnpm lock): Update file

* fix(pnpm lock): Back to main version

* chore(pnpm lock): Add @ecomplus/utils to the loyalty points app

[skip ci]
  • Loading branch information
wisley7l authored Oct 6, 2023
1 parent 47a03e6 commit 4eb705c
Show file tree
Hide file tree
Showing 7 changed files with 208 additions and 33 deletions.
3 changes: 2 additions & 1 deletion packages/apps/loyalty-points/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -25,7 +25,8 @@
},
"dependencies": {
"@cloudcommerce/api": "workspace:*",
"@cloudcommerce/firebase": "workspace:*"
"@cloudcommerce/firebase": "workspace:*",
"@ecomplus/utils": "1.5.0-rc.5"
},
"devDependencies": {
"@cloudcommerce/test-base": "workspace:*",
Expand Down
104 changes: 104 additions & 0 deletions packages/apps/loyalty-points/src/functions-lib/cron-add-points.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import type { CustomerSet, Orders } from '@cloudcommerce/types';
import logger from 'firebase-functions/logger';
import { getFirestore } from 'firebase-admin/firestore';
import api from '@cloudcommerce/api';
import { Endpoint } from '@cloudcommerce/api/types';

const addPoints = async () => {
const d = new Date();
// double checking paid orders after 5 days
d.setDate(d.getDate() - 5);
const db = getFirestore();
const snapshot = await db.collection('pointsToAdd')
.where('queuedAt', '<=', d)
.orderBy('queuedAt')
.get();
const { docs } = snapshot;
logger.info(`${docs.length} points to add`);

for (let i = 0; i < docs.length; i++) {
const { customerId, pointEntries } = docs[i].data();
const orderId = docs[i].ref.id;
let order: Orders | undefined;
try {
// eslint-disable-next-line no-await-in-loop
order = (await api.get(`orders/${orderId}`)).data;
} catch (error: any) {
const status = error.response?.status;
if (status > 400 && status < 500) {
logger.warn(`failed reading order ${orderId}`, {
status,
response: error.response?.data,
});
} else {
throw error;
}
}

if (order && Array.isArray(pointEntries) && pointEntries.length) {
const currentStatus = order.financial_status && order.financial_status.current;
if (currentStatus === 'paid') {
const tryAddPoints = (data: CustomerSet) => {
logger.info(`POST ${JSON.stringify(data)} ${customerId}`);
return api.post(`customers/${customerId}/loyalty_points_entries`, data);
};

const pointsEndpoint = `/customers/${customerId}/loyalty_points_entries`;

for (let ii = 0; ii < pointEntries.length; ii++) {
const pointsEntry = pointEntries[ii];
try {
// eslint-disable-next-line no-await-in-loop
await tryAddPoints(pointsEntry);
} catch (err: any) {
const status = err.response?.status;
if (status === 403) {
// delete older points entry and retry

const findUrl = `${pointsEndpoint}`
+ `?valid_thru<=${(new Date().toISOString())}&sort=active_points&limit=1`;
try {
// eslint-disable-next-line no-await-in-loop
await api.get(findUrl as Endpoint)
.then(({ data }) => {
const pointsList = data.result;
if (pointsList.length) {
const endpoint = `${pointsEndpoint}/${pointsList[0]._id}`;
return api.delete(endpoint as Endpoint)
.then(() => {
return tryAddPoints(pointsEntry);
});
}
return null;
});
} catch (error: any) {
logger.warn(`failed cleaning/adding points retry to ${orderId}`, {
customerId,
pointsEntry,
url: error.config?.url,
method: error.config?.method,
status: error.response?.status,
response: error.response?.data,
});
}
} else if (status > 400 && status < 500) {
logger.warn(`failed adding points to ${orderId}`, {
customerId,
pointsEndpoint,
pointsEntry,
status,
response: err.response.data,
});
} else {
throw err;
}
}
}
}
}
// eslint-disable-next-line no-await-in-loop
await docs[i].ref.delete();
}
};

export default addPoints;
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import type { Orders, Customers, ResourceId } from '@cloudcommerce/types';
import api from '@cloudcommerce/api';
import { getFirestore } from 'firebase-admin/firestore';
import { getFirestore, Timestamp } from 'firebase-admin/firestore';
import logger from 'firebase-functions/logger';
import ecomUtils from '@ecomplus/utils';
import getProgramId from './get-program-id';

const ECHO_SUCCESS = 'SUCCESS';
Expand Down Expand Up @@ -97,36 +98,77 @@ const handleLoyaltyPointsEvent = async (
const hasEarnedPoints = haveEarnedPoints(pointsList, orderId);

if (isPaid && !hasEarnedPoints) {
for (let i = 0; i < programRules.length; i++) {
const rule = programRules[i];
if (amount.subtotal) {
if (!rule.min_subtotal_to_earn || rule.min_subtotal_to_earn <= amount.subtotal) {
const pointsValue = ((rule.earn_percentage || 1) / 100)
* (amount.subtotal - (amount.discount || 0));
const docRef = getFirestore().doc(`pointsToAdd/${orderId}`);
const docSnapshot = await docRef.get();
if (!docSnapshot.exists) {
const pointEntries: any[] = [];

for (let i = 0; i < programRules.length; i++) {
const rule = programRules[i];
if (rule.earn_percentage === 0) {
continue;
}
if (
!rule.min_subtotal_to_earn
|| (amount?.subtotal && rule.min_subtotal_to_earn <= amount.subtotal)
) {
let subtotal = amount.subtotal as number;

let validThru: string | undefined;
if (rule.expiration_days > 0) {
const d = new Date();
d.setDate(d.getDate() + rule.expiration_days);
validThru = d.toISOString();
}
if (Array.isArray(rule.category_ids) && rule.category_ids.length) {
if (!order.items || !order.items.length) {
continue;
}

const data = {
name: rule.name,
program_id: getProgramId(rule, i),
earned_points: pointsValue,
active_points: pointsValue,
ratio: rule.ratio || 1,
valid_thru: validThru,
order_id: orderId,
} as any; // TODO: set the correct type
// eslint-disable-next-line no-await-in-loop
const { data: { result } } = await api.get('search/v1', {
limit: order.items.length,
params: {
_id: order.items.map((item) => item.product_id),
'categories._id': rule.category_ids,
},
});

// eslint-disable-next-line no-await-in-loop
await api.post(`customers/${customerId}/loyalty_points_entries`, data);
// eslint-disable-next-line no-await-in-loop
order.items.forEach((item) => {
if (!result.find(({ _id }) => _id === item.product_id)) {
subtotal -= (ecomUtils.price(item) * item.quantity);
}
});
}

return responsePubSub(ECHO_SUCCESS);
const pointsValue = ((rule.earn_percentage || 1) / 100)
* (subtotal - (amount.discount || 0));

if (pointsValue > 0) {
let validThru: string | undefined;
if (rule.expiration_days > 0) {
const d = new Date();
d.setDate(d.getDate() + 2 + rule.expiration_days);
validThru = d.toISOString();
}
const data = {
name: rule.name,
program_id: getProgramId(rule, i),
earned_points: pointsValue,
active_points: pointsValue,
ratio: rule.ratio || 1,
order_id: orderId,
};
if (validThru) {
Object.assign(data, { valid_thru: validThru });
}
pointEntries.push(data);
}
}
}

await docRef.set({
customerId,
pointEntries,
queuedAt: Timestamp.now(),
});

return responsePubSub(ECHO_SUCCESS);
}
}

Expand Down
15 changes: 11 additions & 4 deletions packages/apps/loyalty-points/src/loyalty-create-transaction.ts
Original file line number Diff line number Diff line change
Expand Up @@ -76,7 +76,7 @@ export default async (appData: AppModuleBody) => {
if (pointsApplied && Array.isArray(programsRules) && programsRules.length) {
// for (const programId in pointsApplied) {
Object.keys(pointsApplied).forEach((programId) => {
const pointsValue = pointsApplied[programId];
let pointsValue = pointsApplied[programId];
if (pointsValue > 0) {
const programRule = programsRules.find((programRuleFound, index) => {
if (programRuleFound) {
Expand All @@ -87,14 +87,21 @@ export default async (appData: AppModuleBody) => {
});

if (programRule) {
let maxPoints = programRule.max_points;
const ratio = programRule.ratio || 1;
if (!maxPoints && programRule.max_amount_percentage && params.amount) {
maxPoints = Math.round(
((programRule.max_amount_percentage * params.amount.total) / 100) / ratio,
);
}
if (maxPoints < pointsValue) {
pointsValue = maxPoints;
}
transaction.loyalty_points = {
name: programRule.name,
program_id: programRule.program_id,
ratio,
points_value: programRule.max_points < pointsValue
? programRule.max_points
: pointsValue,
points_value: pointsValue,
};
transaction.amount = pointsValue * ratio;
}
Expand Down
11 changes: 9 additions & 2 deletions packages/apps/loyalty-points/src/loyalty-list-payments.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,18 @@ export default (data: AppModuleBody) => {
if (Array.isArray(appData.programs_rules)) {
const pointsPrograms = {};
appData.programs_rules.forEach((programRule, index) => {
let maxPoints = programRule.max_points;
const ratio = programRule.ratio || 1;
if (!maxPoints && programRule.max_amount_percentage && params.amount) {
maxPoints = Math.round(
((programRule.max_amount_percentage * params.amount.total) / 100) / ratio,
);
}
const programId = getProgramId(programRule, index);
pointsPrograms[programId] = {
name: programRule.name,
ratio: programRule.ratio || 1,
max_points: programRule.max_points,
ratio,
max_points: maxPoints,
min_subtotal_to_earn: programRule.min_subtotal_to_earn,
earn_percentage: programRule.earn_percentage,
};
Expand Down
13 changes: 12 additions & 1 deletion packages/apps/loyalty-points/src/loyalty-points-events.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,12 @@ import {
ApiEventHandler,
} from '@cloudcommerce/firebase/lib/helpers/pubsub';
import logger from 'firebase-functions/logger';
import config from '@cloudcommerce/firebase/lib/config';
import functions from 'firebase-functions/v1';
import handleLoyaltyPointsEvent from './functions-lib/handle-loyalty-points-event';
import addPoints from './functions-lib/cron-add-points';

const { httpsFunctionOptions: { region } } = config.get();

const handleApiEvent: ApiEventHandler = async ({
evName,
Expand Down Expand Up @@ -37,5 +42,11 @@ export const loyaltypoints = {
onStoreEvent: createAppEventsFunction(
'loyaltyPoints',
handleApiEvent,
) as any,
),

cronAddPoints: functions.region(region).pubsub
.schedule('28 * * * *')
.onRun(() => {
return addPoints;
}),
};
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

0 comments on commit 4eb705c

Please sign in to comment.