@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.
- package/.turbo/turbo-build.log +4 -0
- package/CHANGELOG.md +1 -0
- package/LICENSE.md +230 -0
- package/README.md +1 -0
- package/events.js +1 -0
- package/lib/functions-lib/get-program-id.d.ts +5 -0
- package/lib/functions-lib/get-program-id.js +20 -0
- package/lib/functions-lib/get-program-id.js.map +1 -0
- package/lib/functions-lib/handle-loyalty-points-event.d.ts +3 -0
- package/lib/functions-lib/handle-loyalty-points-event.js +172 -0
- package/lib/functions-lib/handle-loyalty-points-event.js.map +1 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -0
- package/lib/loyalty-create-transaction.d.ts +76 -0
- package/lib/loyalty-create-transaction.js +210 -0
- package/lib/loyalty-create-transaction.js.map +1 -0
- package/lib/loyalty-list-payments.d.ts +3 -0
- package/lib/loyalty-list-payments.js +44 -0
- package/lib/loyalty-list-payments.js.map +1 -0
- package/lib/loyalty-points-events.d.ts +4 -0
- package/lib/loyalty-points-events.js +28 -0
- package/lib/loyalty-points-events.js.map +1 -0
- package/lib/loyalty-points.d.ts +77 -0
- package/lib/loyalty-points.js +12 -0
- package/lib/loyalty-points.js.map +1 -0
- package/package.json +32 -0
- package/src/functions-lib/get-program-id.ts +23 -0
- package/src/functions-lib/handle-loyalty-points-event.ts +207 -0
- package/src/index.ts +1 -0
- package/src/loyalty-create-transaction.ts +241 -0
- package/src/loyalty-list-payments.ts +55 -0
- package/src/loyalty-points-events.ts +41 -0
- package/src/loyalty-points.ts +12 -0
- 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
|
+
};
|