@commercetools/checkout-payments-processor-sdk 0.0.6
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/CHANGELOG.md +15 -0
- package/README.md +42 -0
- package/biome.json +38 -0
- package/package.json +48 -0
- package/src/api/context/request-context.helper.test.ts +98 -0
- package/src/api/context/request-context.helper.ts +89 -0
- package/src/api/context/request-context.provider.ts +27 -0
- package/src/api/context/types/request-context.type.ts +18 -0
- package/src/api/handlers/payment-components.handler.test.ts +378 -0
- package/src/api/handlers/payment-components.handler.ts +80 -0
- package/src/api/handlers/payment-intents.handler.test.ts +475 -0
- package/src/api/handlers/payment-intents.handler.ts +169 -0
- package/src/api/handlers/status.handler.test.ts +551 -0
- package/src/api/handlers/status.handler.ts +216 -0
- package/src/api/handlers/transaction.handler.ts +86 -0
- package/src/api/handlers/types/handler.type.ts +49 -0
- package/src/api/hooks/authorize.hook.test.ts +62 -0
- package/src/api/hooks/authorize.hook.ts +51 -0
- package/src/api/hooks/jwt-auth.hook.test.ts +54 -0
- package/src/api/hooks/jwt-auth.hook.ts +56 -0
- package/src/api/hooks/oauth2-auth.hook.test.ts +173 -0
- package/src/api/hooks/oauth2-auth.hook.ts +46 -0
- package/src/api/hooks/session-header-auth.hook.ts +47 -0
- package/src/api/hooks/session-query-param-auth.hook.ts +46 -0
- package/src/api/hooks/types/hook.type.ts +12 -0
- package/src/api/index.ts +11 -0
- package/src/api/types.ts +3 -0
- package/src/commercetools/api/base-api.ts +28 -0
- package/src/commercetools/api/cart-api.test.ts +95 -0
- package/src/commercetools/api/cart-api.ts +55 -0
- package/src/commercetools/api/custom-type-api.ts +39 -0
- package/src/commercetools/api/order-api.ts +22 -0
- package/src/commercetools/api/payment-api.ts +64 -0
- package/src/commercetools/api/payment-method-api.ts +118 -0
- package/src/commercetools/api/root-api.ts +109 -0
- package/src/commercetools/errors/ct-api.error.ts +27 -0
- package/src/commercetools/helpers/currency.converter.test.ts +121 -0
- package/src/commercetools/helpers/currency.converter.test_data.json +2379 -0
- package/src/commercetools/helpers/currency.converter.ts +102 -0
- package/src/commercetools/helpers/taxrate.converter.test.ts +48 -0
- package/src/commercetools/helpers/taxrate.converter.ts +39 -0
- package/src/commercetools/index.ts +22 -0
- package/src/commercetools/services/ct-cart.service.test.ts +1616 -0
- package/src/commercetools/services/ct-cart.service.ts +316 -0
- package/src/commercetools/services/ct-custom-type.service.ts +62 -0
- package/src/commercetools/services/ct-order.service.test.ts +136 -0
- package/src/commercetools/services/ct-order.service.ts +47 -0
- package/src/commercetools/services/ct-payment-method.service.test.ts +878 -0
- package/src/commercetools/services/ct-payment-method.service.ts +223 -0
- package/src/commercetools/services/ct-payment.service.test.ts +1123 -0
- package/src/commercetools/services/ct-payment.service.ts +574 -0
- package/src/commercetools/types/api.type.ts +162 -0
- package/src/commercetools/types/cart.type.ts +83 -0
- package/src/commercetools/types/custom-type.type.ts +21 -0
- package/src/commercetools/types/order.type.ts +19 -0
- package/src/commercetools/types/payment-method.type.ts +130 -0
- package/src/commercetools/types/payment.type.ts +74 -0
- package/src/commercetools/types/predefined-custom-types.type.ts +240 -0
- package/src/commercetools/types.ts +7 -0
- package/src/errorx/errorx.ts +625 -0
- package/src/errorx/index.ts +1 -0
- package/src/errorx/types.ts +5 -0
- package/src/fetch/decorators/base.decorator.ts +13 -0
- package/src/fetch/decorators/monitoring.decorator.ts +41 -0
- package/src/fetch/fetch.test.ts +57 -0
- package/src/fetch/index.ts +3 -0
- package/src/fetch/types/fetch.type.ts +11 -0
- package/src/fetch/types.ts +1 -0
- package/src/index.ts +15 -0
- package/src/logger/commercetools-logger.ts +69 -0
- package/src/logger/index.ts +1 -0
- package/src/logger/types/logger.type.ts +6 -0
- package/src/logger/types.ts +1 -0
- package/src/mocks/auth.mock.ts +6 -0
- package/src/mocks/cart.mock.ts +239 -0
- package/src/mocks/ct-api-error.mock.ts +23 -0
- package/src/mocks/index.ts +4 -0
- package/src/mocks/payment.mock.ts +35 -0
- package/src/payment-processor.ts +317 -0
- package/src/security/authn/authns.ts +219 -0
- package/src/security/authn/bearer-utils.test.ts +35 -0
- package/src/security/authn/bearer-utils.ts +28 -0
- package/src/security/authn/jwt-authn-manager.ts +69 -0
- package/src/security/authn/oauth2-authn-manager.ts +105 -0
- package/src/security/authn/session-header-authn-manager.ts +58 -0
- package/src/security/authn/session-query-param-authn-manager.ts +53 -0
- package/src/security/authn/types/authn.type.ts +50 -0
- package/src/security/authz/authorization-manager.ts +39 -0
- package/src/security/authz/types/authz.type.ts +13 -0
- package/src/security/index.ts +13 -0
- package/src/security/services/authorization.service.ts +54 -0
- package/src/security/services/jwt.service.test.ts +27 -0
- package/src/security/services/jwt.service.ts +45 -0
- package/src/security/services/oauth2.service.ts +60 -0
- package/src/security/services/session.service.ts +141 -0
- package/src/security/services/types/authorization.type.ts +10 -0
- package/src/security/services/types/jwt.type.ts +3 -0
- package/src/security/services/types/oauth2.type.ts +15 -0
- package/src/security/services/types/session.type.ts +38 -0
- package/src/security/types.ts +6 -0
- package/tsconfig.json +28 -0
- package/tsconfig.prod.json +9 -0
- package/typedoc.json +6 -0
- package/vitest.config.ts +15 -0
- package/wiki/getting-started.md +13 -0
|
@@ -0,0 +1,878 @@
|
|
|
1
|
+
import { randomUUID } from 'node:crypto';
|
|
2
|
+
import { RequestContextProvider } from '@api';
|
|
3
|
+
import type {
|
|
4
|
+
PaymentMethod,
|
|
5
|
+
PaymentMethodPagedQueryResponse,
|
|
6
|
+
PaymentMethodUpdateAction,
|
|
7
|
+
} from '@commercetools/platform-sdk';
|
|
8
|
+
import {
|
|
9
|
+
ErrorInternalConstraintViolated,
|
|
10
|
+
ErrorResourceNotFound,
|
|
11
|
+
} from '@errorx';
|
|
12
|
+
import { CommercetoolsLogger } from '@logger';
|
|
13
|
+
import { CommercetoolsPaymentMethodAPI } from '@/commercetools/api/payment-method-api.js';
|
|
14
|
+
import { DefaultCommercetoolsAPI } from '@/commercetools/api/root-api.js';
|
|
15
|
+
import { DefaultPaymentMethodService } from '@/commercetools/services/ct-payment-method.service.js';
|
|
16
|
+
import type { SavePaymentMethodDraft } from '@/commercetools/types/payment-method.type.js';
|
|
17
|
+
|
|
18
|
+
const projectKey = 'test-payment-methods';
|
|
19
|
+
const contextProvider = new RequestContextProvider({
|
|
20
|
+
getContextFn: () => ({
|
|
21
|
+
correlationId: 'correlation-id',
|
|
22
|
+
requestId: 'request-id',
|
|
23
|
+
}),
|
|
24
|
+
updateContextFn: () => {},
|
|
25
|
+
});
|
|
26
|
+
const logger = new CommercetoolsLogger({ contextProvider, projectKey });
|
|
27
|
+
|
|
28
|
+
const ctAPI = new DefaultCommercetoolsAPI({
|
|
29
|
+
apiUrl: 'http://api.test.com',
|
|
30
|
+
authUrl: 'http://auth.test.com',
|
|
31
|
+
clientId: 'clientId',
|
|
32
|
+
clientSecret: 'clientSecret',
|
|
33
|
+
projectKey,
|
|
34
|
+
contextProvider,
|
|
35
|
+
logger,
|
|
36
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
37
|
+
httpClient: (input: string | URL | Request, init?: RequestInit) => {
|
|
38
|
+
return fetch(input, init);
|
|
39
|
+
},
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
const paymentMethodAPI = new CommercetoolsPaymentMethodAPI({
|
|
43
|
+
client: ctAPI.client,
|
|
44
|
+
});
|
|
45
|
+
ctAPI.paymentMethod = paymentMethodAPI;
|
|
46
|
+
|
|
47
|
+
const ctPaymentMethodService = new DefaultPaymentMethodService({
|
|
48
|
+
ctAPI,
|
|
49
|
+
logger,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
describe('commercetools payment-method service', () => {
|
|
53
|
+
beforeEach(() => {
|
|
54
|
+
vi.clearAllMocks();
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
describe('get', () => {
|
|
58
|
+
test('should return a payment method', async () => {
|
|
59
|
+
//Given
|
|
60
|
+
const paymentId = randomUUID();
|
|
61
|
+
const customerId = randomUUID();
|
|
62
|
+
const paymentInterface = 'psp-interface';
|
|
63
|
+
const paymentMethod: PaymentMethod = {
|
|
64
|
+
id: paymentId,
|
|
65
|
+
version: 1,
|
|
66
|
+
default: false,
|
|
67
|
+
paymentMethodStatus: 'Active',
|
|
68
|
+
createdAt: '',
|
|
69
|
+
lastModifiedAt: '',
|
|
70
|
+
customer: {
|
|
71
|
+
id: customerId,
|
|
72
|
+
typeId: 'customer',
|
|
73
|
+
},
|
|
74
|
+
paymentInterface,
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
const spyer = vi.spyOn(paymentMethodAPI, 'find').mockResolvedValueOnce({
|
|
78
|
+
count: 1,
|
|
79
|
+
limit: 100,
|
|
80
|
+
offset: 0,
|
|
81
|
+
results: [paymentMethod],
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
//When
|
|
85
|
+
const result = await ctPaymentMethodService.get({
|
|
86
|
+
id: paymentId,
|
|
87
|
+
customerId: customerId,
|
|
88
|
+
paymentInterface,
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
//Then
|
|
92
|
+
expect(result).toEqual(paymentMethod);
|
|
93
|
+
|
|
94
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
95
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterface}"`,
|
|
96
|
+
});
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
test('should return the payment-method even if the interfaceAccount is set on the payment-method but it is not provided as a query param since it is an optional value', async () => {
|
|
100
|
+
//Given
|
|
101
|
+
const paymentId = randomUUID();
|
|
102
|
+
const customerId = randomUUID();
|
|
103
|
+
const paymentInterface = 'psp-interface';
|
|
104
|
+
const interfaceAccountValue = 'interfaceaccount-value';
|
|
105
|
+
const paymentMethod: PaymentMethod = {
|
|
106
|
+
id: paymentId,
|
|
107
|
+
version: 1,
|
|
108
|
+
default: false,
|
|
109
|
+
paymentMethodStatus: 'Active',
|
|
110
|
+
createdAt: '',
|
|
111
|
+
lastModifiedAt: '',
|
|
112
|
+
customer: {
|
|
113
|
+
id: customerId,
|
|
114
|
+
typeId: 'customer',
|
|
115
|
+
},
|
|
116
|
+
paymentInterface: paymentInterface,
|
|
117
|
+
interfaceAccount: interfaceAccountValue,
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
const spyer = vi.spyOn(paymentMethodAPI, 'find').mockResolvedValueOnce({
|
|
121
|
+
count: 1,
|
|
122
|
+
limit: 100,
|
|
123
|
+
offset: 0,
|
|
124
|
+
results: [paymentMethod],
|
|
125
|
+
});
|
|
126
|
+
|
|
127
|
+
//When
|
|
128
|
+
const result = await ctPaymentMethodService.get({
|
|
129
|
+
id: paymentId,
|
|
130
|
+
customerId: customerId,
|
|
131
|
+
paymentInterface: paymentInterface,
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
//Then
|
|
135
|
+
expect(result).toEqual(paymentMethod);
|
|
136
|
+
|
|
137
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
138
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterface}"`,
|
|
139
|
+
});
|
|
140
|
+
});
|
|
141
|
+
|
|
142
|
+
test('should throw an ErrorResourceNotFound if no payment-method could be found', async () => {
|
|
143
|
+
//Given
|
|
144
|
+
const paymentId = randomUUID();
|
|
145
|
+
const customerId = randomUUID();
|
|
146
|
+
const paymentInterface = 'psp-interface';
|
|
147
|
+
|
|
148
|
+
const spyer = vi.spyOn(paymentMethodAPI, 'find').mockResolvedValueOnce({
|
|
149
|
+
count: 0,
|
|
150
|
+
limit: 100,
|
|
151
|
+
offset: 0,
|
|
152
|
+
results: [],
|
|
153
|
+
});
|
|
154
|
+
|
|
155
|
+
//When
|
|
156
|
+
const result = ctPaymentMethodService.get({
|
|
157
|
+
id: paymentId,
|
|
158
|
+
customerId: customerId,
|
|
159
|
+
paymentInterface,
|
|
160
|
+
});
|
|
161
|
+
|
|
162
|
+
//Then
|
|
163
|
+
await expect(result).rejects.toThrow(
|
|
164
|
+
new ErrorResourceNotFound('payment-method', paymentId),
|
|
165
|
+
);
|
|
166
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
167
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterface}"`,
|
|
168
|
+
});
|
|
169
|
+
});
|
|
170
|
+
});
|
|
171
|
+
|
|
172
|
+
describe('find', () => {
|
|
173
|
+
test('should return an empty list if nothing matches', async () => {
|
|
174
|
+
//Given
|
|
175
|
+
const customerId = randomUUID();
|
|
176
|
+
const paymentInterfaceValue = 'paymentInterface-value';
|
|
177
|
+
|
|
178
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
179
|
+
limit: 20,
|
|
180
|
+
offset: 0,
|
|
181
|
+
count: 0,
|
|
182
|
+
total: 0,
|
|
183
|
+
results: [],
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
const spyer = vi
|
|
187
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
188
|
+
.mockResolvedValueOnce(queryResults);
|
|
189
|
+
|
|
190
|
+
//When
|
|
191
|
+
const result = await ctPaymentMethodService.find({
|
|
192
|
+
customerId: customerId,
|
|
193
|
+
paymentInterface: paymentInterfaceValue,
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
//Then
|
|
197
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
198
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}"`,
|
|
199
|
+
});
|
|
200
|
+
expect(result).toEqual(queryResults);
|
|
201
|
+
});
|
|
202
|
+
|
|
203
|
+
test('should return a list of payment methods that match with the given "customerId" and "paymentInterface"', async () => {
|
|
204
|
+
//Given
|
|
205
|
+
const customerId = randomUUID();
|
|
206
|
+
const paymentId = randomUUID();
|
|
207
|
+
const paymentInterfaceValue = 'paymentInterface-value';
|
|
208
|
+
|
|
209
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
210
|
+
limit: 100,
|
|
211
|
+
offset: 0,
|
|
212
|
+
count: 1,
|
|
213
|
+
total: 1,
|
|
214
|
+
results: [
|
|
215
|
+
{
|
|
216
|
+
id: paymentId,
|
|
217
|
+
version: 1,
|
|
218
|
+
default: false,
|
|
219
|
+
paymentMethodStatus: 'Active',
|
|
220
|
+
paymentInterface: paymentInterfaceValue,
|
|
221
|
+
createdAt: '',
|
|
222
|
+
lastModifiedAt: '',
|
|
223
|
+
customer: {
|
|
224
|
+
id: customerId,
|
|
225
|
+
typeId: 'customer',
|
|
226
|
+
},
|
|
227
|
+
},
|
|
228
|
+
],
|
|
229
|
+
};
|
|
230
|
+
|
|
231
|
+
const spyer = vi
|
|
232
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
233
|
+
.mockResolvedValueOnce(queryResults);
|
|
234
|
+
|
|
235
|
+
//When
|
|
236
|
+
const result = await ctPaymentMethodService.find({
|
|
237
|
+
customerId: customerId,
|
|
238
|
+
paymentInterface: paymentInterfaceValue,
|
|
239
|
+
});
|
|
240
|
+
|
|
241
|
+
//Then
|
|
242
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
243
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}"`,
|
|
244
|
+
});
|
|
245
|
+
expect(result).toEqual(queryResults);
|
|
246
|
+
});
|
|
247
|
+
|
|
248
|
+
test('should return a list of payment methods that match with the given "customerId", "paymentInterface" and "interfaceAccount"', async () => {
|
|
249
|
+
//Given
|
|
250
|
+
const customerId = randomUUID();
|
|
251
|
+
const paymentId = randomUUID();
|
|
252
|
+
const paymentInterfaceValue = 'paymentInterface-value';
|
|
253
|
+
const interfaceAccountValue = 'interfaceAccount-value';
|
|
254
|
+
|
|
255
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
256
|
+
limit: 100,
|
|
257
|
+
offset: 0,
|
|
258
|
+
count: 1,
|
|
259
|
+
total: 1,
|
|
260
|
+
results: [
|
|
261
|
+
{
|
|
262
|
+
id: paymentId,
|
|
263
|
+
version: 1,
|
|
264
|
+
default: false,
|
|
265
|
+
paymentMethodStatus: 'Active',
|
|
266
|
+
paymentInterface: paymentInterfaceValue,
|
|
267
|
+
interfaceAccount: interfaceAccountValue,
|
|
268
|
+
createdAt: '',
|
|
269
|
+
lastModifiedAt: '',
|
|
270
|
+
customer: {
|
|
271
|
+
id: customerId,
|
|
272
|
+
typeId: 'customer',
|
|
273
|
+
},
|
|
274
|
+
},
|
|
275
|
+
],
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const spyer = vi
|
|
279
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
280
|
+
.mockResolvedValueOnce(queryResults);
|
|
281
|
+
|
|
282
|
+
//When
|
|
283
|
+
const result = await ctPaymentMethodService.find({
|
|
284
|
+
customerId: customerId,
|
|
285
|
+
paymentInterface: paymentInterfaceValue,
|
|
286
|
+
interfaceAccount: interfaceAccountValue,
|
|
287
|
+
});
|
|
288
|
+
|
|
289
|
+
//Then
|
|
290
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
291
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}" and interfaceAccount="${interfaceAccountValue}"`,
|
|
292
|
+
});
|
|
293
|
+
expect(result).toEqual(queryResults);
|
|
294
|
+
});
|
|
295
|
+
});
|
|
296
|
+
|
|
297
|
+
describe('save', () => {
|
|
298
|
+
test('should throw an error if an payment method already exists for that customer with the same token value', async () => {
|
|
299
|
+
//Given
|
|
300
|
+
const draft: SavePaymentMethodDraft = {
|
|
301
|
+
customerId: 'customerId',
|
|
302
|
+
method: 'scheme',
|
|
303
|
+
paymentInterface: 'paymentInterface',
|
|
304
|
+
token: 'token',
|
|
305
|
+
interfaceAccount: 'interfaceAccount',
|
|
306
|
+
};
|
|
307
|
+
|
|
308
|
+
const spyer = vi
|
|
309
|
+
.spyOn(ctPaymentMethodService, 'doesTokenBelongsToCustomer')
|
|
310
|
+
.mockResolvedValueOnce(true);
|
|
311
|
+
|
|
312
|
+
//When
|
|
313
|
+
const result = ctPaymentMethodService.save(draft);
|
|
314
|
+
|
|
315
|
+
//Then
|
|
316
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
317
|
+
customerId: 'customerId',
|
|
318
|
+
paymentInterface: 'paymentInterface',
|
|
319
|
+
interfaceAccount: 'interfaceAccount',
|
|
320
|
+
tokenValue: 'token',
|
|
321
|
+
});
|
|
322
|
+
await expect(result).rejects.toThrow(
|
|
323
|
+
new ErrorInternalConstraintViolated(
|
|
324
|
+
'A payment method with the same "token.value" already exists.',
|
|
325
|
+
{
|
|
326
|
+
privateFields: {
|
|
327
|
+
customerId: 'customerId',
|
|
328
|
+
interfaceAccount: 'interfaceAccount',
|
|
329
|
+
paymentInterface: 'paymentInterface',
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
),
|
|
333
|
+
);
|
|
334
|
+
});
|
|
335
|
+
|
|
336
|
+
test('should pass the draft to the underlying API layer and return the result unchanged', async () => {
|
|
337
|
+
//Given
|
|
338
|
+
const draft: SavePaymentMethodDraft = {
|
|
339
|
+
customerId: 'customerId',
|
|
340
|
+
method: 'scheme',
|
|
341
|
+
paymentInterface: 'paymentInterface',
|
|
342
|
+
token: 'token',
|
|
343
|
+
interfaceAccount: 'interfaceAccount',
|
|
344
|
+
};
|
|
345
|
+
|
|
346
|
+
const paymentId = randomUUID();
|
|
347
|
+
|
|
348
|
+
const paymentMethod: PaymentMethod = {
|
|
349
|
+
id: paymentId,
|
|
350
|
+
version: 1,
|
|
351
|
+
default: false,
|
|
352
|
+
paymentMethodStatus: 'Active',
|
|
353
|
+
createdAt: '',
|
|
354
|
+
lastModifiedAt: '',
|
|
355
|
+
customer: {
|
|
356
|
+
id: 'customerId',
|
|
357
|
+
typeId: 'customer',
|
|
358
|
+
},
|
|
359
|
+
paymentInterface: 'paymentInterface',
|
|
360
|
+
interfaceAccount: 'interfaceAccount',
|
|
361
|
+
token: {
|
|
362
|
+
value: 'token',
|
|
363
|
+
},
|
|
364
|
+
};
|
|
365
|
+
|
|
366
|
+
vi.spyOn(
|
|
367
|
+
ctPaymentMethodService,
|
|
368
|
+
'doesTokenBelongsToCustomer',
|
|
369
|
+
).mockResolvedValueOnce(false);
|
|
370
|
+
|
|
371
|
+
const spyer = vi
|
|
372
|
+
.spyOn(paymentMethodAPI, 'create')
|
|
373
|
+
.mockResolvedValueOnce(paymentMethod);
|
|
374
|
+
|
|
375
|
+
//When
|
|
376
|
+
const result = await ctPaymentMethodService.save(draft);
|
|
377
|
+
|
|
378
|
+
//Then
|
|
379
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
380
|
+
customer: {
|
|
381
|
+
id: 'customerId',
|
|
382
|
+
typeId: 'customer',
|
|
383
|
+
},
|
|
384
|
+
paymentInterface: 'paymentInterface',
|
|
385
|
+
interfaceAccount: 'interfaceAccount',
|
|
386
|
+
method: 'scheme',
|
|
387
|
+
default: false,
|
|
388
|
+
paymentMethodStatus: 'Active',
|
|
389
|
+
token: {
|
|
390
|
+
value: 'token',
|
|
391
|
+
},
|
|
392
|
+
});
|
|
393
|
+
expect(result).toStrictEqual(paymentMethod);
|
|
394
|
+
});
|
|
395
|
+
});
|
|
396
|
+
|
|
397
|
+
describe('update', () => {
|
|
398
|
+
test('should throw an ErrorInternalConstraintViolated if the provided customerId does not match the the given paymentMethodId', async () => {
|
|
399
|
+
//Given
|
|
400
|
+
const paymentId = randomUUID();
|
|
401
|
+
const customerId = randomUUID();
|
|
402
|
+
|
|
403
|
+
const spyer = vi
|
|
404
|
+
.spyOn(paymentMethodAPI, 'checkIfExistsByPredicate')
|
|
405
|
+
.mockResolvedValueOnce(false);
|
|
406
|
+
|
|
407
|
+
//When
|
|
408
|
+
const result = ctPaymentMethodService.update({
|
|
409
|
+
actions: [],
|
|
410
|
+
customerId: customerId,
|
|
411
|
+
resource: {
|
|
412
|
+
id: paymentId,
|
|
413
|
+
version: 1,
|
|
414
|
+
},
|
|
415
|
+
});
|
|
416
|
+
|
|
417
|
+
//Then
|
|
418
|
+
await expect(result).rejects.toThrow(
|
|
419
|
+
new ErrorInternalConstraintViolated(
|
|
420
|
+
`The provided payment-method '${paymentId}' does not have a customer '${customerId}' set`,
|
|
421
|
+
{
|
|
422
|
+
privateFields: {
|
|
423
|
+
predicateQueryString: `id="${paymentId}" and customer(id="${customerId}")`,
|
|
424
|
+
},
|
|
425
|
+
fields: {
|
|
426
|
+
customerId,
|
|
427
|
+
paymentMethodId: paymentId,
|
|
428
|
+
},
|
|
429
|
+
},
|
|
430
|
+
),
|
|
431
|
+
);
|
|
432
|
+
|
|
433
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
434
|
+
queryString: `id="${paymentId}" and customer(id="${customerId}")`,
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
test('should pass the update actions to the underlying API layer and return the result unchanged', async () => {
|
|
439
|
+
//Given
|
|
440
|
+
const paymentId = randomUUID();
|
|
441
|
+
const customerId = randomUUID();
|
|
442
|
+
|
|
443
|
+
const paymentMethod: PaymentMethod = {
|
|
444
|
+
id: paymentId,
|
|
445
|
+
version: 1,
|
|
446
|
+
default: false,
|
|
447
|
+
paymentMethodStatus: 'Active',
|
|
448
|
+
createdAt: '',
|
|
449
|
+
lastModifiedAt: '',
|
|
450
|
+
customer: {
|
|
451
|
+
id: customerId,
|
|
452
|
+
typeId: 'customer',
|
|
453
|
+
},
|
|
454
|
+
};
|
|
455
|
+
|
|
456
|
+
vi.spyOn(
|
|
457
|
+
paymentMethodAPI,
|
|
458
|
+
'checkIfExistsByPredicate',
|
|
459
|
+
).mockResolvedValueOnce(true);
|
|
460
|
+
const spyer = vi
|
|
461
|
+
.spyOn(paymentMethodAPI, 'update')
|
|
462
|
+
.mockResolvedValueOnce(paymentMethod);
|
|
463
|
+
|
|
464
|
+
//When
|
|
465
|
+
const updateActions: PaymentMethodUpdateAction[] = [
|
|
466
|
+
{
|
|
467
|
+
action: 'setInterfaceAccount',
|
|
468
|
+
interfaceAccount: 'new-interfaceAccount-value',
|
|
469
|
+
},
|
|
470
|
+
];
|
|
471
|
+
const result = await ctPaymentMethodService.update({
|
|
472
|
+
actions: updateActions,
|
|
473
|
+
customerId: customerId,
|
|
474
|
+
resource: {
|
|
475
|
+
id: paymentId,
|
|
476
|
+
version: 1,
|
|
477
|
+
},
|
|
478
|
+
});
|
|
479
|
+
|
|
480
|
+
//Then
|
|
481
|
+
expect(result).toEqual(paymentMethod);
|
|
482
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
483
|
+
actions: updateActions,
|
|
484
|
+
resource: {
|
|
485
|
+
id: paymentId,
|
|
486
|
+
version: paymentMethod.version,
|
|
487
|
+
},
|
|
488
|
+
});
|
|
489
|
+
});
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
describe('delete', () => {
|
|
493
|
+
test('should throw an ErrorInternalConstraintViolated if the provided customerId does not match the the given paymentMethodId', async () => {
|
|
494
|
+
//Given
|
|
495
|
+
const paymentId = randomUUID();
|
|
496
|
+
const customerId = randomUUID();
|
|
497
|
+
|
|
498
|
+
const spyer = vi
|
|
499
|
+
.spyOn(paymentMethodAPI, 'checkIfExistsByPredicate')
|
|
500
|
+
.mockResolvedValueOnce(false);
|
|
501
|
+
|
|
502
|
+
//When
|
|
503
|
+
const result = ctPaymentMethodService.delete({
|
|
504
|
+
customerId: customerId,
|
|
505
|
+
id: paymentId,
|
|
506
|
+
version: 1,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
//Then
|
|
510
|
+
await expect(result).rejects.toThrow(
|
|
511
|
+
new ErrorInternalConstraintViolated(
|
|
512
|
+
`The provided payment-method '${paymentId}' does not have a customer '${customerId}' set`,
|
|
513
|
+
{
|
|
514
|
+
privateFields: {
|
|
515
|
+
predicateQueryString: `id="${paymentId}" and customer(id="${customerId}")`,
|
|
516
|
+
},
|
|
517
|
+
fields: {
|
|
518
|
+
customerId: customerId,
|
|
519
|
+
paymentMethodId: paymentId,
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
),
|
|
523
|
+
);
|
|
524
|
+
|
|
525
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
526
|
+
queryString: `id="${paymentId}" and customer(id="${customerId}")`,
|
|
527
|
+
});
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
test('should pass the delete params to the underlying API layer and return the result unchanged', async () => {
|
|
531
|
+
//Given
|
|
532
|
+
const paymentId = randomUUID();
|
|
533
|
+
const customerId = randomUUID();
|
|
534
|
+
|
|
535
|
+
const paymentMethod: PaymentMethod = {
|
|
536
|
+
id: paymentId,
|
|
537
|
+
version: 1,
|
|
538
|
+
default: false,
|
|
539
|
+
paymentMethodStatus: 'Active',
|
|
540
|
+
createdAt: '',
|
|
541
|
+
lastModifiedAt: '',
|
|
542
|
+
customer: {
|
|
543
|
+
id: customerId,
|
|
544
|
+
typeId: 'customer',
|
|
545
|
+
},
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
vi.spyOn(
|
|
549
|
+
paymentMethodAPI,
|
|
550
|
+
'checkIfExistsByPredicate',
|
|
551
|
+
).mockResolvedValueOnce(true);
|
|
552
|
+
const spyer = vi
|
|
553
|
+
.spyOn(paymentMethodAPI, 'delete')
|
|
554
|
+
.mockResolvedValueOnce(paymentMethod);
|
|
555
|
+
|
|
556
|
+
//When
|
|
557
|
+
const result = await ctPaymentMethodService.delete({
|
|
558
|
+
customerId: customerId,
|
|
559
|
+
id: paymentId,
|
|
560
|
+
version: 1,
|
|
561
|
+
});
|
|
562
|
+
|
|
563
|
+
//Then
|
|
564
|
+
expect(result).toStrictEqual(paymentMethod);
|
|
565
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
566
|
+
id: paymentId,
|
|
567
|
+
version: 1,
|
|
568
|
+
});
|
|
569
|
+
});
|
|
570
|
+
});
|
|
571
|
+
|
|
572
|
+
describe('doesTokenBelongsToCustomer', () => {
|
|
573
|
+
test('should return "true" if the token belongs to the customer', async () => {
|
|
574
|
+
//Given
|
|
575
|
+
const paymentId = randomUUID();
|
|
576
|
+
const customerId = randomUUID();
|
|
577
|
+
const paymentInterfaceValue = 'psp-interface';
|
|
578
|
+
const interfaceAccountValue = 'interfaceaccount-value';
|
|
579
|
+
const tokenValue = 'tokenValue';
|
|
580
|
+
|
|
581
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
582
|
+
limit: 100,
|
|
583
|
+
offset: 0,
|
|
584
|
+
count: 2,
|
|
585
|
+
total: 2,
|
|
586
|
+
results: [
|
|
587
|
+
{
|
|
588
|
+
id: paymentId,
|
|
589
|
+
version: 1,
|
|
590
|
+
default: false,
|
|
591
|
+
paymentMethodStatus: 'Active',
|
|
592
|
+
paymentInterface: paymentInterfaceValue,
|
|
593
|
+
interfaceAccount: interfaceAccountValue,
|
|
594
|
+
createdAt: '',
|
|
595
|
+
lastModifiedAt: '',
|
|
596
|
+
customer: {
|
|
597
|
+
id: customerId,
|
|
598
|
+
typeId: 'customer',
|
|
599
|
+
},
|
|
600
|
+
token: {
|
|
601
|
+
value: 'differentTokenValue',
|
|
602
|
+
},
|
|
603
|
+
},
|
|
604
|
+
{
|
|
605
|
+
id: 'db745569-56d8-4a2f-b78e-3f0cee37dd86',
|
|
606
|
+
version: 1,
|
|
607
|
+
default: false,
|
|
608
|
+
paymentMethodStatus: 'Active',
|
|
609
|
+
paymentInterface: paymentInterfaceValue,
|
|
610
|
+
interfaceAccount: interfaceAccountValue,
|
|
611
|
+
createdAt: '',
|
|
612
|
+
lastModifiedAt: '',
|
|
613
|
+
customer: {
|
|
614
|
+
id: customerId,
|
|
615
|
+
typeId: 'customer',
|
|
616
|
+
},
|
|
617
|
+
token: {
|
|
618
|
+
value: tokenValue,
|
|
619
|
+
},
|
|
620
|
+
},
|
|
621
|
+
],
|
|
622
|
+
};
|
|
623
|
+
|
|
624
|
+
const spyer = vi
|
|
625
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
626
|
+
.mockResolvedValueOnce(queryResults);
|
|
627
|
+
|
|
628
|
+
//When
|
|
629
|
+
const result = await ctPaymentMethodService.doesTokenBelongsToCustomer({
|
|
630
|
+
tokenValue: tokenValue,
|
|
631
|
+
interfaceAccount: interfaceAccountValue,
|
|
632
|
+
customerId: customerId,
|
|
633
|
+
paymentInterface: paymentInterfaceValue,
|
|
634
|
+
});
|
|
635
|
+
|
|
636
|
+
//Then
|
|
637
|
+
expect(result).toBeTruthy();
|
|
638
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
639
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}" and interfaceAccount="${interfaceAccountValue}"`,
|
|
640
|
+
});
|
|
641
|
+
});
|
|
642
|
+
|
|
643
|
+
test('should return "false" if token does not belong to the customer', async () => {
|
|
644
|
+
//Given
|
|
645
|
+
const paymentId = randomUUID();
|
|
646
|
+
const customerId = randomUUID();
|
|
647
|
+
const paymentInterfaceValue = 'psp-interface';
|
|
648
|
+
const interfaceAccountValue = 'interfaceaccount-value';
|
|
649
|
+
const tokenValue = 'tokenValue';
|
|
650
|
+
|
|
651
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
652
|
+
limit: 100,
|
|
653
|
+
offset: 0,
|
|
654
|
+
count: 1,
|
|
655
|
+
total: 1,
|
|
656
|
+
results: [
|
|
657
|
+
{
|
|
658
|
+
id: paymentId,
|
|
659
|
+
version: 1,
|
|
660
|
+
default: false,
|
|
661
|
+
paymentMethodStatus: 'Active',
|
|
662
|
+
paymentInterface: paymentInterfaceValue,
|
|
663
|
+
interfaceAccount: interfaceAccountValue,
|
|
664
|
+
createdAt: '',
|
|
665
|
+
lastModifiedAt: '',
|
|
666
|
+
customer: {
|
|
667
|
+
id: customerId,
|
|
668
|
+
typeId: 'customer',
|
|
669
|
+
},
|
|
670
|
+
token: {
|
|
671
|
+
value: tokenValue,
|
|
672
|
+
},
|
|
673
|
+
},
|
|
674
|
+
],
|
|
675
|
+
};
|
|
676
|
+
|
|
677
|
+
const spyer = vi
|
|
678
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
679
|
+
.mockResolvedValueOnce(queryResults);
|
|
680
|
+
|
|
681
|
+
//When
|
|
682
|
+
const result = await ctPaymentMethodService.doesTokenBelongsToCustomer({
|
|
683
|
+
tokenValue: 'differentTokenValue',
|
|
684
|
+
interfaceAccount: interfaceAccountValue,
|
|
685
|
+
customerId: customerId,
|
|
686
|
+
paymentInterface: paymentInterfaceValue,
|
|
687
|
+
});
|
|
688
|
+
|
|
689
|
+
//Then
|
|
690
|
+
expect(result).toBeFalsy();
|
|
691
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
692
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}" and interfaceAccount="${interfaceAccountValue}"`,
|
|
693
|
+
});
|
|
694
|
+
});
|
|
695
|
+
});
|
|
696
|
+
|
|
697
|
+
describe('getByTokenValue', () => {
|
|
698
|
+
test('should return the payment method if the provided tokenValue matches with an payment method which has the corresponding token value set', async () => {
|
|
699
|
+
//Given
|
|
700
|
+
const paymentId = randomUUID();
|
|
701
|
+
const customerId = randomUUID();
|
|
702
|
+
const paymentInterfaceValue = 'psp-interface';
|
|
703
|
+
const interfaceAccountValue = 'interfaceaccount-value';
|
|
704
|
+
const tokenValue = 'tokenValue';
|
|
705
|
+
|
|
706
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
707
|
+
limit: 100,
|
|
708
|
+
offset: 0,
|
|
709
|
+
count: 1,
|
|
710
|
+
total: 1,
|
|
711
|
+
results: [
|
|
712
|
+
{
|
|
713
|
+
id: paymentId,
|
|
714
|
+
version: 1,
|
|
715
|
+
default: false,
|
|
716
|
+
paymentMethodStatus: 'Active',
|
|
717
|
+
paymentInterface: paymentInterfaceValue,
|
|
718
|
+
interfaceAccount: interfaceAccountValue,
|
|
719
|
+
createdAt: '',
|
|
720
|
+
lastModifiedAt: '',
|
|
721
|
+
customer: {
|
|
722
|
+
id: customerId,
|
|
723
|
+
typeId: 'customer',
|
|
724
|
+
},
|
|
725
|
+
token: {
|
|
726
|
+
value: tokenValue,
|
|
727
|
+
},
|
|
728
|
+
},
|
|
729
|
+
],
|
|
730
|
+
};
|
|
731
|
+
|
|
732
|
+
const spyer = vi
|
|
733
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
734
|
+
.mockResolvedValueOnce(queryResults);
|
|
735
|
+
|
|
736
|
+
//When
|
|
737
|
+
const result = await ctPaymentMethodService.getByTokenValue({
|
|
738
|
+
tokenValue: tokenValue,
|
|
739
|
+
interfaceAccount: interfaceAccountValue,
|
|
740
|
+
customerId: customerId,
|
|
741
|
+
paymentInterface: paymentInterfaceValue,
|
|
742
|
+
});
|
|
743
|
+
|
|
744
|
+
//Then
|
|
745
|
+
expect(result).toStrictEqual(queryResults.results[0]);
|
|
746
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
747
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}" and interfaceAccount="${interfaceAccountValue}"`,
|
|
748
|
+
});
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
test.skip('should throw an "ErrorInternalConstraintViolated" if more then one payment method with the same token value is found', async () => {
|
|
752
|
+
//Given
|
|
753
|
+
const paymentId = randomUUID();
|
|
754
|
+
const customerId = randomUUID();
|
|
755
|
+
const paymentInterfaceValue = 'psp-interface';
|
|
756
|
+
const interfaceAccountValue = 'interfaceaccount-value';
|
|
757
|
+
const tokenValue = 'tokenValue';
|
|
758
|
+
|
|
759
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
760
|
+
limit: 100,
|
|
761
|
+
offset: 0,
|
|
762
|
+
count: 2,
|
|
763
|
+
total: 2,
|
|
764
|
+
results: [
|
|
765
|
+
{
|
|
766
|
+
id: paymentId,
|
|
767
|
+
version: 1,
|
|
768
|
+
default: false,
|
|
769
|
+
paymentMethodStatus: 'Active',
|
|
770
|
+
paymentInterface: paymentInterfaceValue,
|
|
771
|
+
interfaceAccount: interfaceAccountValue,
|
|
772
|
+
createdAt: '',
|
|
773
|
+
lastModifiedAt: '',
|
|
774
|
+
customer: {
|
|
775
|
+
id: customerId,
|
|
776
|
+
typeId: 'customer',
|
|
777
|
+
},
|
|
778
|
+
token: {
|
|
779
|
+
value: tokenValue,
|
|
780
|
+
},
|
|
781
|
+
},
|
|
782
|
+
{
|
|
783
|
+
id: '28e2fadb-2f3d-493c-ab75-d912cc2375f5',
|
|
784
|
+
version: 1,
|
|
785
|
+
default: false,
|
|
786
|
+
paymentMethodStatus: 'Active',
|
|
787
|
+
paymentInterface: paymentInterfaceValue,
|
|
788
|
+
interfaceAccount: interfaceAccountValue,
|
|
789
|
+
createdAt: '',
|
|
790
|
+
lastModifiedAt: '',
|
|
791
|
+
customer: {
|
|
792
|
+
id: customerId,
|
|
793
|
+
typeId: 'customer',
|
|
794
|
+
},
|
|
795
|
+
token: {
|
|
796
|
+
value: tokenValue,
|
|
797
|
+
},
|
|
798
|
+
},
|
|
799
|
+
],
|
|
800
|
+
};
|
|
801
|
+
|
|
802
|
+
const spyer = vi
|
|
803
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
804
|
+
.mockResolvedValueOnce(queryResults);
|
|
805
|
+
|
|
806
|
+
//When
|
|
807
|
+
const result = ctPaymentMethodService.getByTokenValue({
|
|
808
|
+
tokenValue: tokenValue,
|
|
809
|
+
interfaceAccount: interfaceAccountValue,
|
|
810
|
+
customerId: customerId,
|
|
811
|
+
paymentInterface: paymentInterfaceValue,
|
|
812
|
+
});
|
|
813
|
+
|
|
814
|
+
//Then
|
|
815
|
+
await expect(result).rejects.toThrow(
|
|
816
|
+
new ErrorInternalConstraintViolated(
|
|
817
|
+
'Found more then one payment method with the same token value.',
|
|
818
|
+
{
|
|
819
|
+
privateFields: {
|
|
820
|
+
customerId,
|
|
821
|
+
interfaceAccount: 'interfaceaccount-value',
|
|
822
|
+
paymentInterface: 'psp-interface',
|
|
823
|
+
paymentMethodIds: [
|
|
824
|
+
paymentId,
|
|
825
|
+
'28e2fadb-2f3d-493c-ab75-d912cc2375f5',
|
|
826
|
+
],
|
|
827
|
+
},
|
|
828
|
+
},
|
|
829
|
+
),
|
|
830
|
+
);
|
|
831
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
832
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}" and interfaceAccount="${interfaceAccountValue}"`,
|
|
833
|
+
});
|
|
834
|
+
});
|
|
835
|
+
|
|
836
|
+
test.skip('should throw an "ErrorResourceNotFound" if no payment method could be found that matches with the given tokenValue', async () => {
|
|
837
|
+
//Given
|
|
838
|
+
const customerId = randomUUID();
|
|
839
|
+
const paymentInterfaceValue = 'psp-interface';
|
|
840
|
+
const interfaceAccountValue = 'interfaceaccount-value';
|
|
841
|
+
const tokenValue = 'tokenValue';
|
|
842
|
+
|
|
843
|
+
const queryResults: PaymentMethodPagedQueryResponse = {
|
|
844
|
+
limit: 100,
|
|
845
|
+
offset: 0,
|
|
846
|
+
count: 0,
|
|
847
|
+
total: 0,
|
|
848
|
+
results: [],
|
|
849
|
+
};
|
|
850
|
+
|
|
851
|
+
const spyer = vi
|
|
852
|
+
.spyOn(paymentMethodAPI, 'find')
|
|
853
|
+
.mockResolvedValueOnce(queryResults);
|
|
854
|
+
|
|
855
|
+
//When
|
|
856
|
+
const result = ctPaymentMethodService.getByTokenValue({
|
|
857
|
+
tokenValue: tokenValue,
|
|
858
|
+
interfaceAccount: interfaceAccountValue,
|
|
859
|
+
customerId: customerId,
|
|
860
|
+
paymentInterface: paymentInterfaceValue,
|
|
861
|
+
});
|
|
862
|
+
|
|
863
|
+
//Then
|
|
864
|
+
await expect(result).rejects.toThrow(
|
|
865
|
+
new ErrorResourceNotFound('customer', customerId, {
|
|
866
|
+
privateFields: {
|
|
867
|
+
customerId: customerId,
|
|
868
|
+
paymentInterface: paymentInterfaceValue,
|
|
869
|
+
interfaceAccount: interfaceAccountValue,
|
|
870
|
+
},
|
|
871
|
+
}),
|
|
872
|
+
);
|
|
873
|
+
expect(spyer).toHaveBeenCalledWith({
|
|
874
|
+
queryString: `customer(id="${customerId}") and paymentInterface="${paymentInterfaceValue}" and interfaceAccount="${interfaceAccountValue}"`,
|
|
875
|
+
});
|
|
876
|
+
});
|
|
877
|
+
});
|
|
878
|
+
});
|