@cloudcommerce/app-loyalty-points 0.1.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (35) hide show
  1. package/.turbo/turbo-build.log +4 -0
  2. package/CHANGELOG.md +1 -0
  3. package/LICENSE.md +230 -0
  4. package/README.md +1 -0
  5. package/events.js +1 -0
  6. package/lib/functions-lib/get-program-id.d.ts +5 -0
  7. package/lib/functions-lib/get-program-id.js +20 -0
  8. package/lib/functions-lib/get-program-id.js.map +1 -0
  9. package/lib/functions-lib/handle-loyalty-points-event.d.ts +3 -0
  10. package/lib/functions-lib/handle-loyalty-points-event.js +172 -0
  11. package/lib/functions-lib/handle-loyalty-points-event.js.map +1 -0
  12. package/lib/index.d.ts +1 -0
  13. package/lib/index.js +2 -0
  14. package/lib/index.js.map +1 -0
  15. package/lib/loyalty-create-transaction.d.ts +76 -0
  16. package/lib/loyalty-create-transaction.js +210 -0
  17. package/lib/loyalty-create-transaction.js.map +1 -0
  18. package/lib/loyalty-list-payments.d.ts +3 -0
  19. package/lib/loyalty-list-payments.js +44 -0
  20. package/lib/loyalty-list-payments.js.map +1 -0
  21. package/lib/loyalty-points-events.d.ts +4 -0
  22. package/lib/loyalty-points-events.js +28 -0
  23. package/lib/loyalty-points-events.js.map +1 -0
  24. package/lib/loyalty-points.d.ts +77 -0
  25. package/lib/loyalty-points.js +12 -0
  26. package/lib/loyalty-points.js.map +1 -0
  27. package/package.json +32 -0
  28. package/src/functions-lib/get-program-id.ts +23 -0
  29. package/src/functions-lib/handle-loyalty-points-event.ts +207 -0
  30. package/src/index.ts +1 -0
  31. package/src/loyalty-create-transaction.ts +241 -0
  32. package/src/loyalty-list-payments.ts +55 -0
  33. package/src/loyalty-points-events.ts +41 -0
  34. package/src/loyalty-points.ts +12 -0
  35. package/tsconfig.json +6 -0
package/src/index.ts ADDED
@@ -0,0 +1 @@
1
+ export * from './loyalty-points';
@@ -0,0 +1,241 @@
1
+ import type {
2
+ AppModuleBody,
3
+ CreateTransactionParams,
4
+ CreateTransactionResponse,
5
+ Customers,
6
+ } from '@cloudcommerce/types';
7
+ import api from '@cloudcommerce/api';
8
+ import { getFirestore } from 'firebase-admin/firestore';
9
+ import logger from 'firebase-functions/logger';
10
+ import getProgramId from './functions-lib/get-program-id';
11
+
12
+ type UsedPointsEntries = Exclude<Customers['loyalty_points_entries'], undefined>[number]
13
+ & { original_active_points: number }
14
+
15
+ const collectionRef = getFirestore().collection('billedPoints');
16
+
17
+ const updateTransactionStatus = (orderId: string) => {
18
+ setTimeout(async () => {
19
+ const order = (await api.get(`orders/${orderId}`)).data;
20
+
21
+ // appSdk.apiRequest(storeId, `/orders/${orderId}.json`).then(({ response }) => {
22
+ const { transactions } = order;
23
+ if (transactions) {
24
+ const transaction = transactions.find((transactionFound) => {
25
+ return transactionFound.payment_method.code === 'loyalty_points';
26
+ });
27
+ if (transaction) {
28
+ const bodyPaymentHistory = {
29
+ transaction_id: transaction._id,
30
+ date_time: new Date().toISOString(),
31
+ status: 'paid',
32
+ customer_notified: true,
33
+ } as any; // TODO: incompatible type=> amount and status;
34
+ await api.post(`orders/${orderId}/payments_history`, bodyPaymentHistory);
35
+ }
36
+ }
37
+ }, 500);
38
+ };
39
+
40
+ const handleUpdatedPoints = (
41
+ endpoint: any, // TODO: endpoint type not string compatible
42
+ body: { [x: string]: any },
43
+ orderId: string,
44
+ usedPointsEntries?: UsedPointsEntries[],
45
+ timeout = 400,
46
+ isLastRequest = false,
47
+ ) => {
48
+ setTimeout(async () => {
49
+ await api.patch(endpoint, body);
50
+ // appSdk.apiRequest(storeId, endpoint, 'PATCH', data).then(() => {
51
+ logger.log(`#(App Loyalty Points): ${orderId} ${endpoint} => ${JSON.stringify(body)}`);
52
+ if (isLastRequest) {
53
+ updateTransactionStatus(orderId);
54
+ collectionRef.doc(orderId).set({ usedPointsEntries });
55
+ }
56
+ // });
57
+ }, timeout);
58
+ };
59
+
60
+ export default async (appData: AppModuleBody) => {
61
+ const { application, storeId } = appData;
62
+ const params = appData.params as CreateTransactionParams;
63
+ // app configured options
64
+ const appConfig = { ...application.data, ...application.hidden_data };
65
+ const { lang, amount } = params;
66
+
67
+ // setup required `transaction` response object
68
+ const transaction: CreateTransactionResponse['transaction'] = {
69
+ amount: amount.total,
70
+ };
71
+
72
+ if (params.payment_method.code === 'loyalty_points') {
73
+ const pointsApplied = params.loyalty_points_applied;
74
+ const programsRules = appConfig.programs_rules;
75
+ if (pointsApplied && Array.isArray(programsRules) && programsRules.length) {
76
+ // for (const programId in pointsApplied) {
77
+ Object.keys(pointsApplied).forEach((programId) => {
78
+ const pointsValue = pointsApplied[programId];
79
+ if (pointsValue > 0) {
80
+ const programRule = programsRules.find((programRuleFound, index) => {
81
+ if (programRuleFound) {
82
+ programRuleFound.program_id = getProgramId(programRuleFound, index);
83
+ return programId === programRuleFound.program_id;
84
+ }
85
+ return false;
86
+ });
87
+
88
+ if (programRule) {
89
+ const ratio = programRule.ratio || 1;
90
+ transaction.loyalty_points = {
91
+ name: programRule.name,
92
+ program_id: programRule.program_id,
93
+ ratio,
94
+ points_value: programRule.max_points < pointsValue
95
+ ? programRule.max_points
96
+ : pointsValue,
97
+ };
98
+ transaction.amount = pointsValue * ratio;
99
+ }
100
+ }
101
+ });
102
+ }
103
+ }
104
+
105
+ if (transaction.amount) {
106
+ const loyaltyPoints = transaction.loyalty_points;
107
+ const customerId = params.buyer.customer_id;
108
+ const orderId = params.order_id as string;
109
+ const usedPointsEntries: UsedPointsEntries[] = [];
110
+ try {
111
+ const customer = (await api.get(`customers/${customerId}`)).data;
112
+ // return appSdk.apiRequest(storeId, `/customers/${customerId}.json`)
113
+ // .then(({ response }) => {
114
+ const pointsEntries = customer.loyalty_points_entries;
115
+ let pointsToConsume = loyaltyPoints?.points_value;
116
+
117
+ if (pointsEntries && pointsToConsume) {
118
+ pointsEntries.sort((a, b) => {
119
+ let value = 0;
120
+ if (a.valid_thru && b.valid_thru) {
121
+ if (a.valid_thru < b.valid_thru) {
122
+ value = -1;
123
+ } else if (a.valid_thru > b.valid_thru) {
124
+ value = 1;
125
+ }
126
+ }
127
+ return value;
128
+ });
129
+
130
+ for (let i = 0; i < pointsEntries.length; i++) {
131
+ if (pointsToConsume <= 0) {
132
+ break;
133
+ }
134
+ const pointsEntry = pointsEntries[i];
135
+ if (pointsEntry.program_id === loyaltyPoints?.program_id) {
136
+ const validThru = pointsEntry.valid_thru;
137
+ const activePoints = pointsEntry.active_points;
138
+ if (activePoints > 0 && (!validThru || new Date(validThru).getTime() >= Date.now())) {
139
+ const pointsDiff = activePoints - pointsToConsume;
140
+ if (pointsDiff > 0 && pointsDiff < 0.01) {
141
+ pointsToConsume = activePoints;
142
+ }
143
+ if (pointsToConsume >= activePoints) {
144
+ pointsToConsume -= activePoints;
145
+ pointsEntry.active_points = 0;
146
+ if (pointsToConsume < 0.02) {
147
+ pointsToConsume = 0;
148
+ }
149
+ } else {
150
+ pointsEntry.active_points -= pointsToConsume;
151
+ pointsToConsume = 0;
152
+ }
153
+ usedPointsEntries.push({
154
+ ...pointsEntry,
155
+ original_active_points: activePoints,
156
+ });
157
+ }
158
+ }
159
+ }
160
+
161
+ if (pointsToConsume <= 0) {
162
+ if (usedPointsEntries.length <= 3) {
163
+ usedPointsEntries.forEach((pointsEntry, i) => {
164
+ /* Obs:
165
+ Store api v2 => loyalty_points_entries do not have id,
166
+ but change can be done by index
167
+ */
168
+ const endpoint = `customers/${customerId}/loyalty_points_entries/${i}`;
169
+ const body = {
170
+ active_points: pointsEntry.active_points,
171
+ };
172
+ handleUpdatedPoints(
173
+ endpoint,
174
+ body,
175
+ orderId,
176
+ usedPointsEntries,
177
+ (i + 1) * 400,
178
+ i + 1 === usedPointsEntries.length,
179
+ );
180
+ });
181
+ } else {
182
+ const endpoint = `customers/${customerId}`;
183
+ const body = {
184
+ loyalty_points_entries: pointsEntries,
185
+ };
186
+ handleUpdatedPoints(endpoint, body, orderId, usedPointsEntries, 400, true);
187
+ }
188
+ }
189
+ }
190
+
191
+ transaction.status = {
192
+ current: pointsToConsume && pointsToConsume <= 0 ? 'authorized' : 'unauthorized',
193
+ };
194
+
195
+ return {
196
+ status: 200,
197
+ redirect_to_payment: false,
198
+ transaction,
199
+ };
200
+ } catch (error: any) {
201
+ // try to debug request error
202
+ const errCode = 'POINTS_TRANSACTION_ERR';
203
+ let { message } = error;
204
+ const err = {
205
+ message: `CREATE_TRANSACTION_ERR #${storeId} - ${orderId} => ${message}`,
206
+ payment: '',
207
+ status: 0,
208
+ response: '',
209
+ usedPointsEntries,
210
+ };
211
+
212
+ if (error.response) {
213
+ const { status, data } = error.response;
214
+ err.status = status;
215
+ if (status !== 401 && status !== 403) {
216
+ if (typeof data === 'object' && data) {
217
+ err.response = JSON.stringify(data);
218
+ } else {
219
+ err.response = data;
220
+ }
221
+ }
222
+ if (data && data.user_message) {
223
+ message = lang ? data.user_message[lang] : data.user_message.en_us;
224
+ }
225
+ }
226
+
227
+ logger.error(err);
228
+
229
+ return {
230
+ error: errCode,
231
+ message,
232
+ };
233
+ }
234
+ }
235
+
236
+ return {
237
+ status: 200,
238
+ redirect_to_payment: false,
239
+ transaction,
240
+ };
241
+ };
@@ -0,0 +1,55 @@
1
+ import type {
2
+ AppModuleBody,
3
+ ListPaymentsResponse,
4
+ ListPaymentsParams,
5
+ } from '@cloudcommerce/types';
6
+ import getProgramId from './functions-lib/get-program-id';
7
+
8
+ export default (data: AppModuleBody) => {
9
+ const { application } = data;
10
+ const params = data.params as ListPaymentsParams;
11
+
12
+ const appData = {
13
+ ...application.data,
14
+ ...application.hidden_data,
15
+ };
16
+
17
+ // const { storeId } = req
18
+ // setup basic required response object
19
+ const response: ListPaymentsResponse = {
20
+ payment_gateways: [],
21
+ };
22
+
23
+ const label = params.lang === 'pt_br' || !params.lang
24
+ ? 'Pontos de fidelidade'
25
+ : 'Loyalty points';
26
+
27
+ response.payment_gateways.push({
28
+ type: 'payment',
29
+ payment_method: {
30
+ code: 'loyalty_points',
31
+ name: label,
32
+ },
33
+ label,
34
+ });
35
+
36
+ if (Array.isArray(appData.programs_rules)) {
37
+ const pointsPrograms = {};
38
+ appData.programs_rules.forEach((programRule, index) => {
39
+ const programId = getProgramId(programRule, index);
40
+ pointsPrograms[programId] = {
41
+ name: programRule.name,
42
+ ratio: programRule.ratio || 1,
43
+ max_points: programRule.max_points,
44
+ min_subtotal_to_earn: programRule.min_subtotal_to_earn,
45
+ earn_percentage: programRule.earn_percentage,
46
+ };
47
+ });
48
+
49
+ if (Object.keys(pointsPrograms).length) {
50
+ response.loyalty_points_programs = pointsPrograms;
51
+ }
52
+ }
53
+
54
+ return response;
55
+ };
@@ -0,0 +1,41 @@
1
+ /* eslint-disable import/prefer-default-export */
2
+ import '@cloudcommerce/firebase/lib/init';
3
+ import type { Orders } from '@cloudcommerce/types';
4
+ import {
5
+ createAppEventsFunction,
6
+ ApiEventHandler,
7
+ } from '@cloudcommerce/firebase/lib/helpers/pubsub';
8
+ import logger from 'firebase-functions/logger';
9
+ import handleLoyaltyPointsEvent from './functions-lib/handle-loyalty-points-event';
10
+
11
+ const handleApiEvent: ApiEventHandler = async ({
12
+ evName,
13
+ apiEvent,
14
+ apiDoc,
15
+ app,
16
+ }) => {
17
+ const resourceId = apiEvent.resource_id;
18
+ logger.info('>> ', resourceId, ' - Action: ', apiEvent.action);
19
+ const key = `${evName}_${resourceId}`;
20
+ const appData = { ...app.data, ...app.hidden_data };
21
+ const programRules = appData.programs_rules;
22
+
23
+ if (
24
+ (Array.isArray(appData.ignore_events)
25
+ && appData.ignore_events.includes(evName))
26
+ || (!Array.isArray(programRules) || !programRules.length)
27
+ ) {
28
+ logger.info('>> ', key, ' - Ignored event');
29
+ return null;
30
+ }
31
+ logger.info(`> Webhook ${resourceId} [${evName}]`);
32
+
33
+ return handleLoyaltyPointsEvent(apiDoc as Orders, programRules);
34
+ };
35
+
36
+ export const loyaltypoints = {
37
+ onStoreEvent: createAppEventsFunction(
38
+ 'loyaltyPoints',
39
+ handleApiEvent,
40
+ ) as any,
41
+ };
@@ -0,0 +1,12 @@
1
+ import '@cloudcommerce/firebase/lib/init';
2
+ import type { AppModuleBody } from '@cloudcommerce/types';
3
+ import handleListPayments from './loyalty-list-payments';
4
+ import handleCreateTransaction from './loyalty-create-transaction';
5
+
6
+ export const listPayments = async (modBody: AppModuleBody) => {
7
+ return handleListPayments(modBody);
8
+ };
9
+
10
+ export const createTransaction = async (modBody: AppModuleBody) => {
11
+ return handleCreateTransaction(modBody);
12
+ };
package/tsconfig.json ADDED
@@ -0,0 +1,6 @@
1
+ {
2
+ "extends": "../../../tsconfig.json",
3
+ "compilerOptions": {
4
+ "declaration": true
5
+ }
6
+ }