@wiicode/youcanpay-sdk 1.0.0

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 (91) hide show
  1. package/README.md +848 -0
  2. package/dist/client.d.ts +17 -0
  3. package/dist/client.js +171 -0
  4. package/dist/client.js.map +1 -0
  5. package/dist/constants.d.ts +8 -0
  6. package/dist/constants.js +12 -0
  7. package/dist/constants.js.map +1 -0
  8. package/dist/enums/currency.enum.d.ts +8 -0
  9. package/dist/enums/currency.enum.js +13 -0
  10. package/dist/enums/currency.enum.js.map +1 -0
  11. package/dist/enums/index.d.ts +2 -0
  12. package/dist/enums/index.js +19 -0
  13. package/dist/enums/index.js.map +1 -0
  14. package/dist/enums/lang.enum.d.ts +5 -0
  15. package/dist/enums/lang.enum.js +10 -0
  16. package/dist/enums/lang.enum.js.map +1 -0
  17. package/dist/errors/index.d.ts +1 -0
  18. package/dist/errors/index.js +18 -0
  19. package/dist/errors/index.js.map +1 -0
  20. package/dist/errors/youcanpay.error.d.ts +16 -0
  21. package/dist/errors/youcanpay.error.js +32 -0
  22. package/dist/errors/youcanpay.error.js.map +1 -0
  23. package/dist/index.d.ts +12 -0
  24. package/dist/index.js +57 -0
  25. package/dist/index.js.map +1 -0
  26. package/dist/interfaces/index.d.ts +5 -0
  27. package/dist/interfaces/index.js +22 -0
  28. package/dist/interfaces/index.js.map +1 -0
  29. package/dist/interfaces/options.interface.d.ts +13 -0
  30. package/dist/interfaces/options.interface.js +3 -0
  31. package/dist/interfaces/options.interface.js.map +1 -0
  32. package/dist/interfaces/payment.interface.d.ts +22 -0
  33. package/dist/interfaces/payment.interface.js +3 -0
  34. package/dist/interfaces/payment.interface.js.map +1 -0
  35. package/dist/interfaces/token.interface.d.ts +26 -0
  36. package/dist/interfaces/token.interface.js +3 -0
  37. package/dist/interfaces/token.interface.js.map +1 -0
  38. package/dist/interfaces/transaction.interface.d.ts +10 -0
  39. package/dist/interfaces/transaction.interface.js +3 -0
  40. package/dist/interfaces/transaction.interface.js.map +1 -0
  41. package/dist/interfaces/webhook.interface.d.ts +16 -0
  42. package/dist/interfaces/webhook.interface.js +10 -0
  43. package/dist/interfaces/webhook.interface.js.map +1 -0
  44. package/dist/logging/index.d.ts +3 -0
  45. package/dist/logging/index.js +20 -0
  46. package/dist/logging/index.js.map +1 -0
  47. package/dist/logging/interfaces.d.ts +22 -0
  48. package/dist/logging/interfaces.js +3 -0
  49. package/dist/logging/interfaces.js.map +1 -0
  50. package/dist/logging/logger.d.ts +6 -0
  51. package/dist/logging/logger.js +37 -0
  52. package/dist/logging/logger.js.map +1 -0
  53. package/dist/logging/sanitizer.d.ts +1 -0
  54. package/dist/logging/sanitizer.js +55 -0
  55. package/dist/logging/sanitizer.js.map +1 -0
  56. package/dist/nestjs/decorators.d.ts +2 -0
  57. package/dist/nestjs/decorators.js +8 -0
  58. package/dist/nestjs/decorators.js.map +1 -0
  59. package/dist/nestjs/guards/index.d.ts +1 -0
  60. package/dist/nestjs/guards/index.js +9 -0
  61. package/dist/nestjs/guards/index.js.map +1 -0
  62. package/dist/nestjs/guards/webhook.guard.d.ts +16 -0
  63. package/dist/nestjs/guards/webhook.guard.js +59 -0
  64. package/dist/nestjs/guards/webhook.guard.js.map +1 -0
  65. package/dist/nestjs/index.d.ts +5 -0
  66. package/dist/nestjs/index.js +22 -0
  67. package/dist/nestjs/index.js.map +1 -0
  68. package/dist/nestjs/pipes/index.d.ts +1 -0
  69. package/dist/nestjs/pipes/index.js +7 -0
  70. package/dist/nestjs/pipes/index.js.map +1 -0
  71. package/dist/nestjs/pipes/webhook.pipe.d.ts +6 -0
  72. package/dist/nestjs/pipes/webhook.pipe.js +34 -0
  73. package/dist/nestjs/pipes/webhook.pipe.js.map +1 -0
  74. package/dist/nestjs/youcanpay.module.d.ts +6 -0
  75. package/dist/nestjs/youcanpay.module.js +48 -0
  76. package/dist/nestjs/youcanpay.module.js.map +1 -0
  77. package/dist/nestjs/youcanpay.service.d.ts +5 -0
  78. package/dist/nestjs/youcanpay.service.js +30 -0
  79. package/dist/nestjs/youcanpay.service.js.map +1 -0
  80. package/dist/security/index.d.ts +2 -0
  81. package/dist/security/index.js +24 -0
  82. package/dist/security/index.js.map +1 -0
  83. package/dist/security/validators.d.ts +37 -0
  84. package/dist/security/validators.js +183 -0
  85. package/dist/security/validators.js.map +1 -0
  86. package/dist/security/webhook.d.ts +80 -0
  87. package/dist/security/webhook.js +146 -0
  88. package/dist/security/webhook.js.map +1 -0
  89. package/dist/tsconfig.build.tsbuildinfo +1 -0
  90. package/dist/tsconfig.tsbuildinfo +1 -0
  91. package/package.json +49 -0
package/README.md ADDED
@@ -0,0 +1,848 @@
1
+ # YouCanPay SDK
2
+
3
+ Production-ready Node.js SDK for [YouCanPay](https://youcanpay.com) - Morocco's payment gateway.
4
+
5
+ Works with **any Node.js framework** (Express, Fastify, Hapi) and has first-class **NestJS integration**.
6
+
7
+ ## Table of Contents
8
+
9
+ - [Installation](#installation)
10
+ - [Environment Setup](#environment-setup)
11
+ - [Quick Start](#quick-start)
12
+ - [Complete Payment Flow](#complete-payment-flow)
13
+ - [API Reference](#api-reference)
14
+ - [Webhook Handling](#webhook-handling)
15
+ - [Database Integration](#database-integration)
16
+ - [Validation & Security](#validation--security)
17
+ - [Error Handling](#error-handling)
18
+ - [Testing](#testing)
19
+
20
+ ---
21
+
22
+ ## Installation
23
+
24
+ ```bash
25
+ npm install youcanpay-sdk
26
+ ```
27
+
28
+ ```bash
29
+ yarn add youcanpay-sdk
30
+ ```
31
+
32
+ ### Peer Dependencies
33
+
34
+ For **NestJS** integration, ensure you have:
35
+ ```bash
36
+ npm install @nestjs/common @nestjs/core
37
+ ```
38
+
39
+ ---
40
+
41
+ ## Environment Setup
42
+
43
+ ### Required Variables
44
+
45
+ ```env
46
+ # YouCanPay API Credentials
47
+ YCP_PRIVATE_KEY=pri_sandbox_xxxxx # Your private key
48
+ YCP_PUBLIC_KEY=pub_sandbox_xxxxx # Your public key
49
+ YCP_SANDBOX=true # true = sandbox, false = production
50
+ ```
51
+
52
+ ### Optional Variables
53
+
54
+ ```env
55
+ # Webhook security (generate a random string)
56
+ YCP_WEBHOOK_SECRET=your_random_secret_here
57
+ ```
58
+
59
+ ### Where to Get API Keys
60
+
61
+ 1. Go to [YouCanPay Dashboard](https://youcanpay.com)
62
+ 2. Create an account or login
63
+ 3. Navigate to **Settings > API Keys**
64
+ 4. Copy your **Sandbox** keys for testing or **Live** keys for production
65
+
66
+ ---
67
+
68
+ ## Quick Start
69
+
70
+ ### Plain Node.js / Express
71
+
72
+ ```typescript
73
+ import { YouCanPayClient, CurrencyCode } from 'youcanpay-sdk';
74
+
75
+ // Initialize client
76
+ const client = new YouCanPayClient({
77
+ privateKey: process.env.YCP_PRIVATE_KEY!,
78
+ publicKey: process.env.YCP_PUBLIC_KEY!,
79
+ sandbox: process.env.YCP_SANDBOX === 'true',
80
+ });
81
+
82
+ // Create a payment
83
+ async function createPayment() {
84
+ const { token } = await client.createToken({
85
+ orderId: 'order-123',
86
+ amount: 50000, // 500.00 MAD (in centimes)
87
+ currency: CurrencyCode.MAD,
88
+ customerIp: '192.168.1.1',
89
+ successUrl: 'https://myapp.com/payment/success',
90
+ errorUrl: 'https://myapp.com/payment/error',
91
+ });
92
+
93
+ // Redirect user to YouCanPay checkout
94
+ const paymentUrl = client.getPaymentUrl(token.id);
95
+ return paymentUrl;
96
+ }
97
+ ```
98
+
99
+ ### NestJS Integration
100
+
101
+ #### Option 1: Static Configuration
102
+
103
+ ```typescript
104
+ // app.module.ts
105
+ import { Module } from '@nestjs/common';
106
+ import { YouCanPayModule } from 'youcanpay-sdk';
107
+
108
+ @Module({
109
+ imports: [
110
+ YouCanPayModule.forRoot({
111
+ privateKey: 'pri_sandbox_xxxxx',
112
+ publicKey: 'pub_sandbox_xxxxx',
113
+ sandbox: true,
114
+ }),
115
+ ],
116
+ })
117
+ export class AppModule {}
118
+ ```
119
+
120
+ #### Option 2: Async Configuration (Recommended)
121
+
122
+ ```typescript
123
+ // app.module.ts
124
+ import { Module } from '@nestjs/common';
125
+ import { ConfigModule, ConfigService } from '@nestjs/config';
126
+ import { YouCanPayModule } from 'youcanpay-sdk';
127
+
128
+ @Module({
129
+ imports: [
130
+ ConfigModule.forRoot(),
131
+ YouCanPayModule.forRootAsync({
132
+ inject: [ConfigService],
133
+ useFactory: (config: ConfigService) => ({
134
+ privateKey: config.get('YCP_PRIVATE_KEY')!,
135
+ publicKey: config.get('YCP_PUBLIC_KEY')!,
136
+ sandbox: config.get('YCP_SANDBOX') === 'true',
137
+ }),
138
+ }),
139
+ ],
140
+ })
141
+ export class AppModule {}
142
+ ```
143
+
144
+ #### Using the Service
145
+
146
+ ```typescript
147
+ // payments.service.ts
148
+ import { Injectable } from '@nestjs/common';
149
+ import { YouCanPayService, CurrencyCode } from 'youcanpay-sdk';
150
+
151
+ @Injectable()
152
+ export class PaymentsService {
153
+ constructor(private readonly youcanpay: YouCanPayService) {}
154
+
155
+ async createPayment(orderId: string, amount: number, customerIp: string) {
156
+ const { token } = await this.youcanpay.createToken({
157
+ orderId,
158
+ amount,
159
+ currency: CurrencyCode.MAD,
160
+ customerIp,
161
+ successUrl: 'https://myapp.com/success',
162
+ errorUrl: 'https://myapp.com/error',
163
+ });
164
+
165
+ return {
166
+ tokenId: token.id,
167
+ paymentUrl: this.youcanpay.getPaymentUrl(token.id),
168
+ };
169
+ }
170
+ }
171
+ ```
172
+
173
+ ---
174
+
175
+ ## Complete Payment Flow
176
+
177
+ ```
178
+ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
179
+ │ User │ │ Your App │ │ SDK │ │ YouCanPay│
180
+ └──────────┘ └──────────┘ └──────────┘ └──────────┘
181
+ │ │ │ │
182
+ │ 1. Click Pay │ │ │
183
+ │───────────────>│ │ │
184
+ │ │ 2. createToken │ │
185
+ │ │───────────────>│ │
186
+ │ │ │ 3. POST /tokenize
187
+ │ │ │───────────────>│
188
+ │ │ │<───────────────│
189
+ │ │<───────────────│ { token } │
190
+ │ │ │ │
191
+ │ │ 4. Save to DB (PENDING) │
192
+ │ │ │ │
193
+ │ 5. Redirect to paymentUrl │ │
194
+ │<───────────────│ │ │
195
+ │ │ │ │
196
+ │ 6. User pays on YouCanPay │ │
197
+ │────────────────────────────────────────────────>│
198
+ │ │ │ │
199
+ │ │ 7. Webhook: transaction.paid │
200
+ │ │<───────────────────────────────│
201
+ │ │ 8. Update DB (COMPLETED) │
202
+ │ │ │ │
203
+ │ 9. Redirect back │ │
204
+ │<────────────────────────────────────────────────│
205
+ │ │ │ │
206
+ │ 10. Verify │ │ │
207
+ │───────────────>│ 11. Check DB │ │
208
+ │<───────────────│ │ │
209
+ │ Success! │ │ │
210
+ ```
211
+
212
+ ### Step-by-Step Implementation
213
+
214
+ #### Step 1-4: Create Payment
215
+
216
+ ```typescript
217
+ // Create payment and store in database
218
+ async function initiatePayment(userId: string, amount: number) {
219
+ const orderId = generateOrderId(); // e.g., UUID
220
+
221
+ // Save to database first
222
+ await db.payment.create({
223
+ orderId,
224
+ amount,
225
+ currency: 'MAD',
226
+ userId,
227
+ status: 'PENDING',
228
+ });
229
+
230
+ // Create token with YouCanPay
231
+ const { token } = await client.createToken({
232
+ orderId,
233
+ amount,
234
+ currency: CurrencyCode.MAD,
235
+ customerIp: getClientIp(),
236
+ successUrl: `https://myapp.com/payment/success`,
237
+ errorUrl: `https://myapp.com/payment/error`,
238
+ });
239
+
240
+ // Update with token ID
241
+ await db.payment.update({
242
+ where: { orderId },
243
+ data: { tokenId: token.id },
244
+ });
245
+
246
+ return client.getPaymentUrl(token.id);
247
+ }
248
+ ```
249
+
250
+ #### Step 7-8: Handle Webhook
251
+
252
+ ```typescript
253
+ import { parseWebhookPayload, verifyWebhookSecret } from 'youcanpay-sdk';
254
+
255
+ app.post('/webhook', async (req, res) => {
256
+ // Verify webhook secret
257
+ const isValid = verifyWebhookSecret({
258
+ secret: process.env.YCP_WEBHOOK_SECRET!,
259
+ query: req.query,
260
+ });
261
+
262
+ if (!isValid) {
263
+ return res.status(401).json({ error: 'Invalid secret' });
264
+ }
265
+
266
+ // Parse webhook payload
267
+ const webhook = parseWebhookPayload(req.body);
268
+
269
+ // Update database
270
+ await db.payment.update({
271
+ where: { orderId: webhook.orderId },
272
+ data: {
273
+ status: webhook.isSuccess ? 'COMPLETED' : 'FAILED',
274
+ transactionId: webhook.transactionId,
275
+ },
276
+ });
277
+
278
+ res.json({ received: true });
279
+ });
280
+ ```
281
+
282
+ #### Step 10-11: Verify Payment
283
+
284
+ ```typescript
285
+ app.get('/payment/success', async (req, res) => {
286
+ const { order_id, transaction_id } = req.query;
287
+
288
+ // NEVER trust URL params - verify from database
289
+ const payment = await db.payment.findUnique({
290
+ where: { orderId: order_id },
291
+ });
292
+
293
+ if (payment?.status === 'COMPLETED') {
294
+ // Payment verified!
295
+ res.render('success', { payment });
296
+ } else {
297
+ // Still pending or failed
298
+ res.render('pending');
299
+ }
300
+ });
301
+ ```
302
+
303
+ ---
304
+
305
+ ## API Reference
306
+
307
+ ### YouCanPayClient / YouCanPayService Methods
308
+
309
+ #### `createToken(params): Promise<TokenResponse>`
310
+
311
+ Create a payment token.
312
+
313
+ ```typescript
314
+ const { token } = await client.createToken({
315
+ orderId: string, // Your unique order ID
316
+ amount: number, // Amount in centimes (5000 = 50.00 MAD)
317
+ currency: CurrencyCode, // 'MAD' | 'USD' | 'EUR'
318
+ customerIp: string, // Customer's IP address
319
+ successUrl: string, // Redirect URL on success
320
+ errorUrl?: string, // Redirect URL on error
321
+ customer?: { // Optional customer info
322
+ name?: string,
323
+ email?: string,
324
+ phone?: string,
325
+ address?: string,
326
+ city?: string,
327
+ state?: string,
328
+ zip_code?: string,
329
+ country_code?: string,
330
+ },
331
+ metadata?: Record<string, string>, // Custom data
332
+ });
333
+ ```
334
+
335
+ #### `getPaymentUrl(tokenId, lang?): string`
336
+
337
+ Get the YouCanPay checkout page URL.
338
+
339
+ ```typescript
340
+ const url = client.getPaymentUrl(token.id, 'fr'); // 'en' | 'fr' | 'ar'
341
+ // Returns: https://youcanpay.com/sandbox/payment-form/{tokenId}?lang=fr
342
+ ```
343
+
344
+ #### `getTransaction(transactionId): Promise<Transaction>`
345
+
346
+ Fetch transaction details from YouCanPay.
347
+
348
+ ```typescript
349
+ const transaction = await client.getTransaction('txn-123');
350
+ // { id, order_id, amount, currency, status, created_at, ... }
351
+ ```
352
+
353
+ #### `payWithCreditCard(params): Promise<PaymentResponse>`
354
+
355
+ Process card payment server-side (for PCI-compliant setups).
356
+
357
+ ```typescript
358
+ const result = await client.payWithCreditCard({
359
+ tokenId: string,
360
+ creditCard: string, // Card number
361
+ expireDate: string, // MM/YY
362
+ cvv: string,
363
+ cardHolderName: string,
364
+ });
365
+ ```
366
+
367
+ #### `payWithCashPlus(params): Promise<CashPlusPaymentResponse>`
368
+
369
+ Initialize CashPlus payment.
370
+
371
+ ```typescript
372
+ const result = await client.payWithCashPlus({
373
+ tokenId: string,
374
+ });
375
+ ```
376
+
377
+ ### Standalone Functions
378
+
379
+ #### Webhook Functions
380
+
381
+ ```typescript
382
+ import {
383
+ parseWebhookPayload,
384
+ verifyWebhookSecret,
385
+ verifyWebhookHMAC,
386
+ createWebhookSignature,
387
+ } from 'youcanpay-sdk';
388
+
389
+ // Parse YouCanPay webhook payload
390
+ const webhook = parseWebhookPayload(requestBody);
391
+ // Returns: ParsedWebhookPayload
392
+
393
+ // Verify webhook secret from query parameter
394
+ const isValid = verifyWebhookSecret({
395
+ secret: 'your-secret',
396
+ query: { secret: '...' }, // From URL query
397
+ headers: { ... }, // Or from headers
398
+ });
399
+
400
+ // HMAC verification (if YouCanPay adds this)
401
+ const isValid = verifyWebhookHMAC(payload, signature, secret, 'sha256');
402
+
403
+ // Create HMAC signature
404
+ const signature = createWebhookSignature(payload, secret);
405
+ ```
406
+
407
+ #### Validation Functions
408
+
409
+ ```typescript
410
+ import {
411
+ validateAmount,
412
+ validateCurrency,
413
+ validateRedirectURL,
414
+ validateOrderId,
415
+ validateIP,
416
+ validateEmail,
417
+ validatePaymentInput,
418
+ } from 'youcanpay-sdk';
419
+
420
+ // All return: { valid: boolean, error?: string }
421
+
422
+ validateAmount(5000); // { valid: true }
423
+ validateAmount(50); // { valid: false, error: 'Amount must be at least 100' }
424
+
425
+ validateCurrency('MAD'); // { valid: true }
426
+ validateCurrency('GBP'); // { valid: false, error: 'Currency must be...' }
427
+
428
+ validateRedirectURL('https://app.com'); // { valid: true }
429
+ validateRedirectURL('javascript:...'); // { valid: false }
430
+
431
+ // Validate all at once
432
+ const result = validatePaymentInput({
433
+ amount: 5000,
434
+ currency: 'MAD',
435
+ orderId: 'order-123',
436
+ successUrl: 'https://myapp.com/success',
437
+ });
438
+ ```
439
+
440
+ #### Utility Functions
441
+
442
+ ```typescript
443
+ import {
444
+ toCentimes,
445
+ fromCentimes,
446
+ formatAmount,
447
+ sanitizeString,
448
+ SUPPORTED_CURRENCIES,
449
+ } from 'youcanpay-sdk';
450
+
451
+ toCentimes(50.00); // 5000
452
+ fromCentimes(5000); // 50.00
453
+ formatAmount(5000, 'MAD'); // "50.00 MAD"
454
+ sanitizeString('<script>'); // "script"
455
+ SUPPORTED_CURRENCIES; // ['MAD', 'USD', 'EUR']
456
+ ```
457
+
458
+ ### TypeScript Interfaces
459
+
460
+ ```typescript
461
+ import type {
462
+ YouCanPayOptions,
463
+ CreateTokenParams,
464
+ TokenResponse,
465
+ Transaction,
466
+ ParsedWebhookPayload,
467
+ ValidationResult,
468
+ CurrencyCode,
469
+ } from 'youcanpay-sdk';
470
+ ```
471
+
472
+ ---
473
+
474
+ ## Webhook Handling
475
+
476
+ ### Webhook Payload Structure
477
+
478
+ YouCanPay sends webhooks with this structure:
479
+
480
+ ```json
481
+ {
482
+ "id": "webhook-uuid",
483
+ "event_name": "transaction.paid",
484
+ "sandbox": true,
485
+ "payload": {
486
+ "transaction": {
487
+ "id": "txn-uuid",
488
+ "status": 1,
489
+ "order_id": "your-order-id",
490
+ "amount": "50000",
491
+ "currency": "MAD",
492
+ "created_at": "2024-01-01T12:00:00.000000Z"
493
+ },
494
+ "payment_method": { ... },
495
+ "customer": { ... }
496
+ }
497
+ }
498
+ ```
499
+
500
+ ### Parsing with SDK
501
+
502
+ ```typescript
503
+ import { parseWebhookPayload, ParsedWebhookPayload } from 'youcanpay-sdk';
504
+
505
+ const webhook: ParsedWebhookPayload = parseWebhookPayload(requestBody);
506
+
507
+ console.log(webhook.transactionId); // 'txn-uuid'
508
+ console.log(webhook.orderId); // 'your-order-id'
509
+ console.log(webhook.amount); // 50000
510
+ console.log(webhook.isSuccess); // true
511
+ console.log(webhook.status); // 'paid' | 'failed' | 'refunded'
512
+ console.log(webhook.eventName); // 'transaction.paid'
513
+ ```
514
+
515
+ ### NestJS Webhook Handler
516
+
517
+ ```typescript
518
+ import {
519
+ Controller,
520
+ Post,
521
+ Body,
522
+ Query,
523
+ UnauthorizedException,
524
+ HttpCode,
525
+ } from '@nestjs/common';
526
+ import {
527
+ ParseWebhookPipe,
528
+ ParsedWebhookPayload,
529
+ verifyWebhookSecret,
530
+ } from 'youcanpay-sdk';
531
+
532
+ @Controller('payments')
533
+ export class PaymentsController {
534
+ @Post('webhook')
535
+ @HttpCode(200)
536
+ async handleWebhook(
537
+ @Body(ParseWebhookPipe) webhook: ParsedWebhookPayload,
538
+ @Query() query: Record<string, string>,
539
+ ) {
540
+ // Verify secret
541
+ if (!verifyWebhookSecret({ secret: process.env.YCP_WEBHOOK_SECRET!, query })) {
542
+ throw new UnauthorizedException('Invalid webhook secret');
543
+ }
544
+
545
+ // Process payment
546
+ if (webhook.isSuccess) {
547
+ await this.paymentService.markCompleted(webhook.orderId, webhook.transactionId);
548
+ } else {
549
+ await this.paymentService.markFailed(webhook.orderId);
550
+ }
551
+
552
+ return { received: true };
553
+ }
554
+ }
555
+ ```
556
+
557
+ ### Webhook Security Checklist
558
+
559
+ - [ ] Add secret to webhook URL: `https://myapp.com/webhook?secret=xxx`
560
+ - [ ] Verify secret before processing
561
+ - [ ] Verify transaction with `getTransaction()` API
562
+ - [ ] Check idempotency (don't process same webhook twice)
563
+ - [ ] Verify amount matches your database
564
+ - [ ] Return 200 OK quickly (process async if needed)
565
+ - [ ] Log webhooks for debugging (sanitize sensitive data)
566
+
567
+ ---
568
+
569
+ ## Database Integration
570
+
571
+ ### Recommended Schema
572
+
573
+ ```sql
574
+ CREATE TABLE payments (
575
+ id UUID PRIMARY KEY,
576
+ order_id VARCHAR(255) UNIQUE NOT NULL,
577
+ token_id VARCHAR(255),
578
+ transaction_id VARCHAR(255),
579
+ amount INTEGER NOT NULL, -- In centimes
580
+ currency VARCHAR(3) NOT NULL,
581
+ status VARCHAR(20) NOT NULL, -- PENDING, COMPLETED, FAILED
582
+ user_id UUID NOT NULL,
583
+ metadata JSONB,
584
+ created_at TIMESTAMP DEFAULT NOW(),
585
+ updated_at TIMESTAMP DEFAULT NOW()
586
+ );
587
+ ```
588
+
589
+ ### Prisma Example
590
+
591
+ ```prisma
592
+ // schema.prisma
593
+ model Payment {
594
+ id String @id @default(uuid())
595
+ orderId String @unique @map("order_id")
596
+ tokenId String? @map("token_id")
597
+ transactionId String? @map("transaction_id")
598
+ amount Int
599
+ currency String
600
+ status String @default("PENDING")
601
+ userId String @map("user_id")
602
+ metadata Json?
603
+ createdAt DateTime @default(now()) @map("created_at")
604
+ updatedAt DateTime @updatedAt @map("updated_at")
605
+
606
+ @@map("payments")
607
+ }
608
+ ```
609
+
610
+ ```typescript
611
+ // Usage
612
+ const payment = await prisma.payment.create({
613
+ data: {
614
+ orderId: 'order-123',
615
+ amount: 50000,
616
+ currency: 'MAD',
617
+ userId: user.id,
618
+ status: 'PENDING',
619
+ },
620
+ });
621
+ ```
622
+
623
+ ### TypeORM Example
624
+
625
+ ```typescript
626
+ @Entity('payments')
627
+ export class Payment {
628
+ @PrimaryGeneratedColumn('uuid')
629
+ id: string;
630
+
631
+ @Column({ unique: true })
632
+ orderId: string;
633
+
634
+ @Column({ nullable: true })
635
+ tokenId: string;
636
+
637
+ @Column({ nullable: true })
638
+ transactionId: string;
639
+
640
+ @Column()
641
+ amount: number;
642
+
643
+ @Column()
644
+ currency: string;
645
+
646
+ @Column({ default: 'PENDING' })
647
+ status: 'PENDING' | 'COMPLETED' | 'FAILED';
648
+
649
+ @Column()
650
+ userId: string;
651
+
652
+ @CreateDateColumn()
653
+ createdAt: Date;
654
+
655
+ @UpdateDateColumn()
656
+ updatedAt: Date;
657
+ }
658
+ ```
659
+
660
+ ### Status Flow
661
+
662
+ ```
663
+ PENDING ──────┬──────> COMPLETED (webhook: transaction.paid)
664
+
665
+ └──────> FAILED (webhook: transaction.failed)
666
+ ```
667
+
668
+ ---
669
+
670
+ ## Validation & Security
671
+
672
+ ### Amount Handling
673
+
674
+ YouCanPay uses **centimes** (smallest currency unit):
675
+
676
+ ```typescript
677
+ import { toCentimes, fromCentimes, formatAmount } from 'youcanpay-sdk';
678
+
679
+ // User enters: 50.00 MAD
680
+ const centimes = toCentimes(50.00); // 5000
681
+
682
+ // Display from API response
683
+ const display = formatAmount(5000, 'MAD'); // "50.00 MAD"
684
+ ```
685
+
686
+ ### Input Validation
687
+
688
+ ```typescript
689
+ import { validatePaymentInput } from 'youcanpay-sdk';
690
+
691
+ const validation = validatePaymentInput({
692
+ amount: userInput.amount,
693
+ currency: userInput.currency,
694
+ successUrl: userInput.successUrl,
695
+ errorUrl: userInput.errorUrl,
696
+ });
697
+
698
+ if (!validation.valid) {
699
+ throw new BadRequestException(validation.error);
700
+ }
701
+ ```
702
+
703
+ ### Security Best Practices
704
+
705
+ 1. **Never trust redirect URL params** - Always verify payment status from your database
706
+ 2. **Validate webhook secrets** - Reject webhooks without valid secret
707
+ 3. **Verify with API** - Call `getTransaction()` to confirm transaction exists
708
+ 4. **Check amounts** - Ensure webhook amount matches your database
709
+ 5. **Idempotency** - Don't process the same webhook twice
710
+ 6. **HTTPS only** - Never use HTTP in production
711
+ 7. **Sanitize inputs** - Use `sanitizeString()` for user inputs
712
+ 8. **URL whitelist** - Validate redirect URLs against allowed domains
713
+
714
+ ---
715
+
716
+ ## Error Handling
717
+
718
+ ### YouCanPayError
719
+
720
+ ```typescript
721
+ import { YouCanPayError, ErrorCodes } from 'youcanpay-sdk';
722
+
723
+ try {
724
+ await client.createToken({ ... });
725
+ } catch (error) {
726
+ if (error instanceof YouCanPayError) {
727
+ console.log(error.code); // ErrorCodes.VALIDATION_ERROR
728
+ console.log(error.status); // 422
729
+ console.log(error.message); // "The amount field is required"
730
+ console.log(error.details); // { amount: ['required'] }
731
+ }
732
+ }
733
+ ```
734
+
735
+ ### Error Codes
736
+
737
+ ```typescript
738
+ enum ErrorCodes {
739
+ NETWORK_ERROR = 'NETWORK_ERROR',
740
+ UNAUTHORIZED = 'UNAUTHORIZED',
741
+ VALIDATION_ERROR = 'VALIDATION_ERROR',
742
+ PAYMENT_FAILED = 'PAYMENT_FAILED',
743
+ UNKNOWN_ERROR = 'UNKNOWN_ERROR',
744
+ }
745
+ ```
746
+
747
+ ### NestJS Exception Filter
748
+
749
+ ```typescript
750
+ @Catch(YouCanPayError)
751
+ export class YouCanPayExceptionFilter implements ExceptionFilter {
752
+ catch(exception: YouCanPayError, host: ArgumentsHost) {
753
+ const response = host.switchToHttp().getResponse();
754
+
755
+ response.status(exception.status || 500).json({
756
+ error: exception.code,
757
+ message: exception.message,
758
+ });
759
+ }
760
+ }
761
+ ```
762
+
763
+ ---
764
+
765
+ ## Testing
766
+
767
+ ### Sandbox Mode
768
+
769
+ Always use sandbox for testing:
770
+
771
+ ```typescript
772
+ const client = new YouCanPayClient({
773
+ privateKey: 'pri_sandbox_xxxxx',
774
+ publicKey: 'pub_sandbox_xxxxx',
775
+ sandbox: true, // <-- Important!
776
+ });
777
+ ```
778
+
779
+ ### Test Card Numbers
780
+
781
+ | Card Number | Result |
782
+ |-------------|--------|
783
+ | `4000 0000 0000 0002` | Success |
784
+ | `4000 0000 0000 0010` | 3D Secure |
785
+ | `4000 0000 0000 0036` | Declined |
786
+
787
+ Use any future expiry date and any 3-digit CVV.
788
+
789
+ ### Mocking the SDK
790
+
791
+ ```typescript
792
+ // Jest mock
793
+ jest.mock('youcanpay-sdk', () => ({
794
+ YouCanPayClient: jest.fn().mockImplementation(() => ({
795
+ createToken: jest.fn().mockResolvedValue({
796
+ token: { id: 'test-token-123' },
797
+ }),
798
+ getPaymentUrl: jest.fn().mockReturnValue('https://youcanpay.com/test'),
799
+ getTransaction: jest.fn().mockResolvedValue({
800
+ id: 'txn-123',
801
+ order_id: 'order-123',
802
+ amount: 5000,
803
+ status: 'paid',
804
+ }),
805
+ })),
806
+ }));
807
+ ```
808
+
809
+ ### Testing Webhooks Locally
810
+
811
+ Use [ngrok](https://ngrok.com) to expose your local server:
812
+
813
+ ```bash
814
+ ngrok http 3000
815
+ # Returns: https://abc123.ngrok.io
816
+
817
+ # Set webhook URL in YouCanPay dashboard:
818
+ # https://abc123.ngrok.io/webhook?secret=your-secret
819
+ ```
820
+
821
+ ---
822
+
823
+ ## Configuration Options
824
+
825
+ ```typescript
826
+ interface YouCanPayOptions {
827
+ privateKey: string; // Required: Your private API key
828
+ publicKey: string; // Required: Your public API key
829
+ sandbox?: boolean; // Use sandbox environment (default: false)
830
+ timeout?: number; // Request timeout in ms (default: 30000)
831
+ logging?: { // Optional audit logging
832
+ enabled: boolean;
833
+ storage: 'database' | 'custom' | 'none';
834
+ handler?: (log: YouCanPayLogEntry) => Promise<void>;
835
+ };
836
+ }
837
+ ```
838
+
839
+ ---
840
+
841
+ ## Support
842
+
843
+ - [YouCanPay Documentation](https://youcanpay.com/docs)
844
+ - [GitHub Issues](https://github.com/your-repo/youcanpay-sdk/issues)
845
+
846
+ ## License
847
+
848
+ MIT