@fat-zebra/sdk 1.0.2 → 1.0.3

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.
@@ -1,514 +0,0 @@
1
- /**
2
- * @jest-environment jsdom
3
- */
4
-
5
- import Sca, { ScaConfig, ScaRunProps } from '..'
6
- import {
7
- Customer,
8
- PaymentIntent,
9
- PublicEvent,
10
- } from '../../shared/types'
11
- import { LocalStorageAccessTokenKey } from '../../shared/constants'
12
- import { mockRequest } from '../../../tests/helpers/api-gateway-mock'
13
- import * as nock from 'nock'
14
- import { CardinalManager } from '../cardinal'
15
- import { ChallengeWindowSize, EnrollSCARequest, ThreedsData } from '../types'
16
- import {
17
- enrollmentScenarios,
18
- EnrollmentScenario,
19
- } from '../scenarios'
20
- import GatewayClient from '../../shared/api-gateway-client'
21
-
22
- jest.mock('../cardinal')
23
- // mock module function
24
- const eventManager = require('../../shared/event-manager')
25
- eventManager.emit = jest.fn()
26
-
27
- interface ResponseOverride {
28
- status: number
29
- body: unknown
30
- }
31
-
32
- const mockCardinalManager = () => {
33
- // @ts-ignore
34
- CardinalManager.mockImplementation(() => {
35
- return {
36
- processBin: () => ['success'],
37
- setup: () => true,
38
- onPaymentSetupComplete: () => {
39
- return {
40
- sessionId: 'session_123'
41
- }
42
- },
43
- onPaymentValidated: () => true,
44
- continue: () => jest.fn()
45
- }
46
- })
47
- }
48
-
49
- const gatewayClient = new GatewayClient({
50
- accessToken: "abc1234",
51
- username: "TEST"
52
- })
53
-
54
- const scaTests = (config: ScaRunProps) => {
55
- const { paymentIntent, cardToken, customer } = config
56
-
57
- const requestCustomer = (customer: ScaRunProps['customer']): EnrollSCARequest['customer'] => {
58
- let properties: EnrollSCARequest['customer'] = {};
59
- if (!customer) return properties;
60
- if (customer.firstName) properties.first_name = customer.firstName
61
- if (customer.lastName) properties.last_name = customer.lastName
62
- if (customer.email) properties.email = customer.email
63
- if (customer.address) properties.address = customer.address
64
- if (customer.city) properties.city = customer.city
65
- if (customer.state) properties.state = customer.state
66
- if (customer.postcode) properties.postcode = customer.postcode
67
- if (customer.country) properties.country = customer.country
68
- return properties
69
- }
70
-
71
- const enrollmentRequestBody: EnrollSCARequest = {
72
- amount: paymentIntent.payment.amount,
73
- card_token: cardToken,
74
- currency: paymentIntent.payment.currency,
75
- reference: paymentIntent.payment.reference,
76
- hide_card_holder: paymentIntent.payment.hide_card_holder,
77
- verification: paymentIntent.verification,
78
- session_id: 'session_123',
79
- challenge_window_size: ChallengeWindowSize.SIZE_FULL_PAGE,
80
- customer: requestCustomer(customer),
81
- }
82
-
83
- const mockCreateSCASessionSuccess = () => {
84
- mockRequest({
85
- request: {
86
- action: 'POST',
87
- path: '/sca/session',
88
- body: {
89
- amount: paymentIntent.payment.amount,
90
- currency: paymentIntent.payment.currency,
91
- hide_card_holder: paymentIntent.payment.hide_card_holder,
92
- }
93
- },
94
- response: {
95
- status: 200,
96
- body: {
97
- data: {
98
- jwt: 'thisisajwt'
99
- }
100
- }
101
- }
102
- })
103
- }
104
-
105
- const mockSCASession = (override?: ResponseOverride) => {
106
- const responseBody = {
107
- data: {
108
- jwt: 'thisisajwt'
109
- }
110
- }
111
-
112
- mockRequest({
113
- request: {
114
- action: 'POST',
115
- path: '/sca/session',
116
- body: {
117
- amount: paymentIntent.payment.amount,
118
- currency: paymentIntent.payment.currency,
119
- hide_card_holder: paymentIntent.payment.hide_card_holder,
120
- }
121
- },
122
- response: {
123
- status: (override ? override.status : 200),
124
- body: (override ? override.body : responseBody)
125
- }
126
- })
127
- }
128
-
129
- const mockSCAEnrollment = (override?: ResponseOverride) => {
130
- const responseBody = {
131
- data: {
132
- action: {
133
- proceed: true
134
- },
135
- version: '1.0',
136
- enrolled: 'Y',
137
- acs_url: '',
138
- pareq: 'asdfasdf',
139
- pares: '',
140
- decision: 'ACCEPT',
141
- }
142
- }
143
-
144
- mockRequest({
145
- request: {
146
- action: 'POST',
147
- path: '/sca/enrollment',
148
- body: enrollmentRequestBody
149
- },
150
- response: {
151
- status: (override ? override.status : 200),
152
- body: (override ? override.body : responseBody)
153
- }
154
- })
155
- }
156
-
157
- describe('SCA session failed', () => {
158
- it('Emits fz.validation.error event with detail', async () => {
159
-
160
- mockRequest({
161
- request: {
162
- action: 'POST',
163
- path: '/sca/session',
164
- body: {
165
- amount: paymentIntent.payment.amount,
166
- currency: paymentIntent.payment.currency,
167
- hide_card_holder: paymentIntent.payment.hide_card_holder,
168
- }
169
- },
170
- response: {
171
- status: 422,
172
- body: {
173
- errors: ['Create SCA session error.']
174
- }
175
- }
176
- })
177
-
178
- mockSCAEnrollment()
179
-
180
- const sca = new Sca({ gatewayClient })
181
-
182
- await sca.run(config)
183
-
184
- const eventDetail: any = {
185
- detail: {
186
- errors: ['FatZebra.3DS: JWT creation failed.'],
187
- data: null
188
- }
189
- }
190
- expect(eventManager.emit).toHaveBeenCalledTimes(1)
191
- expect(eventManager.emit.mock.calls[0][0]).toBe(PublicEvent.SCA_ERROR)
192
- expect(eventManager.emit.mock.calls[0][1]).toMatchObject(eventDetail.detail)
193
- })
194
- })
195
-
196
- describe('Bin processing failed', () => {
197
- it('Emits fz.validation.error event without detail', async () => {
198
- //@ts-ignore
199
- CardinalManager.mockImplementation(() => {
200
- return {
201
- processBin: () => {
202
- throw new Error('GG')
203
- },
204
- setup: () => true,
205
- onPaymentSetupComplete: () => {
206
- return {
207
- sessionId: '123'
208
- }
209
- },
210
- onPaymentValidated: () => true
211
- }
212
- })
213
-
214
- mockCreateSCASessionSuccess()
215
- mockSCAEnrollment()
216
-
217
- const sca = new Sca({ gatewayClient })
218
- sca.cardinal = new CardinalManager()
219
- await sca.run(config)
220
-
221
- const eventDetail: any = {
222
- detail: {
223
- errors: ['FatZebra.3DS: BIN verification failed.'],
224
- data: null
225
- }
226
- }
227
-
228
- expect(eventManager.emit).toHaveBeenCalledTimes(1)
229
- expect(eventManager.emit.mock.calls[0][0]).toBe(PublicEvent.SCA_ERROR)
230
- expect(eventManager.emit.mock.calls[0][1]).toMatchObject(eventDetail.detail)
231
- })
232
- })
233
-
234
- describe('3DS version 2', () => {
235
- const scenarios = enrollmentScenarios.filter((item) => item.threedsVersion[0] == '2')
236
-
237
- scenarios.forEach((scenario: EnrollmentScenario) => {
238
- describe(`Enrollment: ${scenario.description}`, () => {
239
- beforeEach(() => {
240
- mockCardinalManager()
241
- mockSCASession()
242
- mockSCAEnrollment({
243
- status: 200,
244
- body: {
245
- data: {
246
- version: '2.1.0',
247
- cavv: 'cavv',
248
- eci: '05',
249
- xid: 'xid',
250
- pareq: 'pareq',
251
- pares: scenario.pares,
252
- enrolled: scenario.veresEnrolled,
253
- reason_code: scenario.reasonCode,
254
- directory_server_txn_id: 'txn123',
255
- ucaf_collection_indicator: 'ucaf123',
256
- }
257
- }
258
- })
259
- })
260
-
261
- it('SCA', async () => {
262
- const sca = new Sca({ gatewayClient})
263
- sca.cardinal = new CardinalManager()
264
- const spy = jest.spyOn(sca.cardinal, 'continue')
265
-
266
- await sca.run(config)
267
-
268
- if (scenario.outcome.authenticationType === 'challenge') {
269
- expect(spy).toHaveBeenCalledTimes(1)
270
- expect(eventManager.emit).toHaveBeenCalledTimes(0)
271
- return
272
- }
273
-
274
- if (scenario.outcome.success) {
275
- const eventDetail = {
276
- detail: {
277
- message: `FatZebra.3DS: 3DS success - ${scenario.description}.`,
278
- data: {
279
- cavv: 'cavv',
280
- par: scenario.pares,
281
- sli: '05',
282
- xid: 'xid',
283
- ver: 'Y',
284
- threedsVersion: '2.1.0',
285
- directoryServerTxnId: 'txn123',
286
- } as ThreedsData,
287
- }
288
- }
289
- expect(eventManager.emit).toHaveBeenCalledTimes(1)
290
- expect(eventManager.emit.mock.calls[0][0]).toBe(PublicEvent.SCA_SUCCESS)
291
- expect(eventManager.emit.mock.calls[0][1]).toMatchObject(eventDetail.detail)
292
- } else {
293
- const eventDetail = {
294
- detail: {
295
- errors: [`FatZebra.3DS: 3DS error - ${scenario.description}`],
296
- data: {
297
- errorCode: scenario.outcome.errorCode
298
- },
299
- }
300
- }
301
- expect(eventManager.emit).toHaveBeenCalledTimes(1)
302
- expect(eventManager.emit.mock.calls[0][0]).toBe(PublicEvent.SCA_ERROR)
303
- expect(eventManager.emit.mock.calls[0][1]).toMatchObject(eventDetail.detail)
304
- }
305
- })
306
- })
307
- })
308
- })
309
-
310
- describe('3DS version 1', () => {
311
- const scenarios = enrollmentScenarios.filter((item) => item.threedsVersion[0] == '1')
312
-
313
- scenarios.forEach((scenario: EnrollmentScenario) => {
314
- describe(`Enrollment: ${scenario.description}`, () => {
315
- beforeEach(() => {
316
- mockCardinalManager()
317
- mockSCASession()
318
- mockSCAEnrollment({
319
- status: 200,
320
- body: {
321
- data: {
322
- version: '1.0.2',
323
- cavv: 'cavv',
324
- eci: '05',
325
- xid: 'xid',
326
- pareq: 'pareq',
327
- pares: scenario.pares,
328
- enrolled: scenario.veresEnrolled,
329
- reason_code: scenario.reasonCode,
330
- directory_server_txn_id: 'txn123',
331
- ucaf_collection_indicator: 'ucaf123',
332
- }
333
- }
334
- })
335
- })
336
-
337
- it('SCA', async () => {
338
- const sca = new Sca({ gatewayClient })
339
- sca.cardinal = new CardinalManager()
340
- const spy = jest.spyOn(sca.cardinal, 'continue')
341
-
342
- await sca.run(config)
343
-
344
- if (scenario.outcome.authenticationType === 'challenge') {
345
- expect(spy).toHaveBeenCalledTimes(1)
346
- expect(eventManager.emit).toHaveBeenCalledTimes(0)
347
- return
348
- }
349
-
350
- if (scenario.outcome.success) {
351
- const eventDetail = {
352
- detail: {
353
- message: `FatZebra.3DS: 3DS success - ${scenario.description}.`,
354
- data: {
355
- cavv: 'cavv',
356
- par: scenario.pares,
357
- sli: '05',
358
- xid: 'xid',
359
- ver: scenario.veresEnrolled,
360
- threedsVersion: '1.0.2',
361
- directoryServerTxnId: 'txn123',
362
- } as ThreedsData,
363
- }
364
- }
365
- expect(eventManager.emit).toHaveBeenCalledTimes(1)
366
- expect(eventManager.emit.mock.calls[0][0]).toBe(PublicEvent.SCA_SUCCESS)
367
- expect(eventManager.emit.mock.calls[0][1]).toMatchObject(eventDetail.detail)
368
- } else {
369
- const eventDetail = {
370
- detail: {
371
- errors: [`FatZebra.3DS: 3DS error - ${scenario.description}`],
372
- data: {
373
- errorCode: scenario.outcome.errorCode
374
- },
375
- }
376
- }
377
- expect(eventManager.emit).toHaveBeenCalledTimes(1)
378
- expect(eventManager.emit.mock.calls[0][0]).toBe(PublicEvent.SCA_ERROR)
379
- expect(eventManager.emit.mock.calls[0][1]).toMatchObject(eventDetail.detail)
380
- }
381
- })
382
- })
383
- })
384
- })
385
- }
386
-
387
- describe('Sca', () => {
388
- beforeEach(() => {
389
- jest.clearAllMocks()
390
- nock.disableNetConnect()
391
- nock.cleanAll()
392
-
393
- mockCardinalManager()
394
-
395
- localStorage.setItem(LocalStorageAccessTokenKey, 'TOKEN')
396
- })
397
-
398
- const bin = '400000'
399
- const cardToken = 'token_12345'
400
- const paymentIntent: PaymentIntent = {
401
- payment: {
402
- amount: 1000,
403
- currency: 'AUD',
404
- reference: 'ref_12345',
405
- hide_card_holder: false,
406
- },
407
- verification: 'ver_12345'
408
- }
409
-
410
- describe('#run', () => {
411
- describe('customer details: full', () => {
412
- const customer: Customer = {
413
- firstName: 'Ziggy',
414
- lastName: 'Zebra',
415
- email: 'ziggy.zebra@fatzebra.com',
416
- address: '2/58-62 Kippax Street',
417
- city: 'Surry Hills',
418
- postcode: '2010',
419
- state: 'NSW',
420
- country: 'Australia'
421
- }
422
- const config = {
423
- bin,
424
- cardToken,
425
- customer,
426
- paymentIntent,
427
- }
428
-
429
- scaTests(config)
430
- })
431
-
432
- describe('customer details: partial', () => {
433
- const customer: Customer = {
434
- firstName: 'Ziggy',
435
- lastName: '',
436
- email: 'ziggy.zebra@fatzebra.com',
437
- address: null,
438
- city: undefined,
439
- postcode: '',
440
- }
441
-
442
- const config = {
443
- bin,
444
- cardToken,
445
- customer,
446
- paymentIntent,
447
- }
448
-
449
- scaTests(config)
450
- })
451
-
452
- describe('customer details: empty', () => {
453
- const customer: Customer = {}
454
-
455
- const config = {
456
- bin,
457
- cardToken,
458
- customer,
459
- paymentIntent,
460
- }
461
-
462
- scaTests(config)
463
- })
464
-
465
- describe('no customer', () => {
466
- const config = {
467
- bin,
468
- cardToken,
469
- paymentIntent,
470
- }
471
-
472
- scaTests(config)
473
- })
474
- })
475
-
476
- describe('#customerProperties', () => {
477
- const sca = new Sca({ gatewayClient })
478
-
479
- it('formats the property keys into snake_case', () => {
480
- const customer: Customer = {
481
- firstName: 'Ziggy',
482
- lastName: 'Zebra',
483
- }
484
-
485
- const expectedProperties = {
486
- first_name: 'Ziggy',
487
- last_name: 'Zebra',
488
- }
489
-
490
- expect(sca.customerProperties(customer)).toEqual(expectedProperties)
491
- })
492
-
493
- it('only retains non-empty customer properties', () => {
494
- const customer: Customer = {
495
- firstName: 'Ziggy',
496
- lastName: 'Zebra',
497
- email: 'ziggy.zebra@fatzebra.com',
498
- address: null,
499
- city: null,
500
- postcode: undefined,
501
- state: undefined,
502
- country: ''
503
- }
504
-
505
- const expectedProperties = {
506
- first_name: 'Ziggy',
507
- last_name: 'Zebra',
508
- email: 'ziggy.zebra@fatzebra.com',
509
- }
510
-
511
- expect(sca.customerProperties(customer)).toEqual(expectedProperties)
512
- })
513
- })
514
- })
@@ -1,136 +0,0 @@
1
- // https://cardinaldocs.atlassian.net/wiki/spaces/CC/pages/1409568/Configurations
2
- interface CardinalConfig {
3
- timeout?: number
4
- maxRequestRetries?: number
5
- logging?: {
6
- level: 'on' | 'off' | 'verbose'
7
- }
8
- }
9
-
10
- interface CardinalContinueObject {
11
- AcsUrl: string
12
- Payload: string
13
- }
14
-
15
- interface CardinalOrderObject {
16
- OrderDetails: {
17
- TransactionId: string
18
- }
19
- }
20
-
21
- interface CardinalSetupCompleteResponseData {
22
- sessionId: string
23
- modules: { module: string, loaded: boolean }[]
24
- }
25
-
26
- // https://cardinaldocs.atlassian.net/wiki/spaces/CC/pages/98315/Response+Objects
27
- interface PaymentValidatedResponseData {
28
- Validated: boolean
29
- ErrorNumber: number
30
- ErrorDescription: string
31
- ActionCode: 'SUCCESS' | 'NOACTION' | 'FAILURE' | 'ERROR'
32
- Payment: {
33
- OrderId: string
34
- OrderNumber: string
35
- ProcessorTransactionId: string
36
- ReasonDescription: string
37
- }
38
- }
39
-
40
- interface PaymentValidatedDTO {
41
- processorTransactionId: string
42
- jwt: string
43
- }
44
-
45
- enum CardinalWorkflow {
46
- CCA = 'cca',
47
- INIT = 'init',
48
- }
49
-
50
- enum CardinalPaymentEvent {
51
- SETUP_COMPLETE = 'payments.setupComplete',
52
- VALIDATED = 'payments.validated',
53
- }
54
-
55
- interface Cardinal {
56
- configure(config: CardinalConfig): void
57
- continue(
58
- workflow: CardinalWorkflow,
59
- data: CardinalContinueObject,
60
- order: CardinalOrderObject
61
- ): void
62
- on(event: string, callback: (...data: any[]) => void): void
63
- setup(workflow: CardinalWorkflow, data: any): void
64
- trigger(event: string, cardBin: string): Promise<any>
65
- }
66
-
67
- interface ScaResult {
68
- success: boolean
69
- type: 'frictionless' | 'challenge' | 'none'
70
- error: string
71
- }
72
-
73
- const defaultConfig = {
74
- timeout: 8000,
75
- maxRequestRetries: 3,
76
- logging: {
77
- level: 'on'
78
- }
79
- }
80
-
81
- export default class CardinalManager {
82
- private cardinal: Cardinal
83
-
84
- constructor(config?: CardinalConfig) {
85
- window.Cardinal.configure((config ? config : defaultConfig as CardinalConfig))
86
- }
87
-
88
- setup(jwt: any): void {
89
- window.Cardinal.setup(CardinalWorkflow.INIT, { jwt })
90
- }
91
-
92
- onPaymentValidated(handler: (data: PaymentValidatedDTO, error: string) => void): void {
93
- window.Cardinal.on('payments.validated', (data: PaymentValidatedResponseData, jwt: string) => {
94
- if (data.ErrorDescription.toLowerCase() === 'success') {
95
- handler({
96
- processorTransactionId: data.Payment.ProcessorTransactionId,
97
- jwt
98
- }, null)
99
- } else {
100
- handler(null, data.ErrorDescription)
101
- }
102
- })
103
- }
104
-
105
- onPaymentSetupComplete(): Promise<CardinalSetupCompleteResponseData> {
106
- return new Promise((resolve, reject) => {
107
- window.Cardinal.on(CardinalPaymentEvent.SETUP_COMPLETE, (data: CardinalSetupCompleteResponseData) => {
108
- resolve(data)
109
- })
110
- })
111
- }
112
-
113
- async processBin(bin: string): Promise<any> {
114
- await window.Cardinal.trigger('bin.process', bin)
115
- }
116
-
117
- continue(acsUrl: string, pareq: string, transactionId: string) {
118
- window.Cardinal.continue(
119
- CardinalWorkflow.CCA,
120
- {
121
- AcsUrl: acsUrl,
122
- Payload: pareq
123
- },
124
- {
125
- OrderDetails: {
126
- TransactionId: transactionId
127
- }
128
- }
129
- )
130
- }
131
- }
132
-
133
- export {
134
- CardinalManager
135
- }
136
- export type { PaymentValidatedDTO }
@@ -1,63 +0,0 @@
1
- // FatZebra only supports SLI 05, 06, 07 & 09.
2
- // https://docs.fatzebra.com/docs/3d-secure
3
- // Cardinal returns ECI(or SLI for MC) in the range of 01~07
4
- // https://cardinaldocs.atlassian.net/wiki/spaces/CCen/pages/903577725/EMV+3DS+2.0+Test+Cases
5
- const eciMappings: { vendorEci: string, fatzebraSli: string }[] = [
6
- {
7
- // Cards: Mastercard, CB Mastercard
8
- // Test Case 2: Failed Frictionless Authentication
9
- // Test Case 4: Unavailable Frictionless Authentication from the Issuer
10
- // Test Case 5: Rejected Frictionless Authentication by the Issuer
11
- // Test Case 6: Authentication Not Available on Lookup
12
- // Test Case 7: Error on Lookup
13
- // Test Case 9: Bypassed Authentication
14
- // Test Case 11: Failed Step Up Authentication
15
- // Test Case 12: Step Up Authentication is Unavailable
16
- // Test Case 13: Error on Authentication
17
- vendorEci: '00',
18
- // Non-authenticated transaction - issuer not support. CAVV not present.
19
- fatzebraSli: '07',
20
- },
21
- {
22
- // Cards: Mastercard, CB Mastercard
23
- // Test Case 3: Attempts Stand-In Frictionless Authentication
24
- vendorEci: '01',
25
- // Cards: Visa, American Express, Discover (Diners Club), CB Visa, ELO
26
- // Secure, Non-authenticated transaction (XID present, CAVV not present)
27
- fatzebraSli: '06',
28
- },
29
- {
30
- // Cards: Mastercard, CB Mastercard
31
- // Test Case 1: Successful Frictionless Authentication
32
- // Test Case 10: Successful Step Up Authentication
33
- vendorEci: '02',
34
- // Cards: Visa, American Express, Discover (Diners Club), CB Visa, ELO
35
- // Secure, Authenticated transaction with XID and CAVV present
36
- fatzebraSli: '05',
37
- },
38
- {
39
- vendorEci: '05',
40
- // Secure, Authenticated transaction with XID and CAVV present
41
- fatzebraSli: '05',
42
- },
43
- {
44
- vendorEci: '06',
45
- // Secure, Non-authenticated transaction (XID present, CAVV not present)
46
- fatzebraSli: '06',
47
- },
48
- {
49
- vendorEci: '07',
50
- // Secure, Non-authenticated transaction (XID and CAVV not present). This is the default value.
51
- fatzebraSli: '07',
52
- },
53
- ]
54
-
55
- const toFzSli = (vendorEci: string): string => {
56
- const matched = eciMappings.find((item) => item.vendorEci == vendorEci)
57
- return matched ? matched.fatzebraSli : vendorEci
58
- }
59
-
60
- export {
61
- eciMappings,
62
- toFzSli,
63
- }