From b4a7c2318aa93afe03cf70ab5d2fa48ca0ed566d Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Mon, 4 May 2026 14:49:05 +0530 Subject: [PATCH 1/7] feat(chore): support retrieval of invoice PDF download URLs support retrieval of invoice PDF download URLs GH-27 --- .../unit/invoice-pdf.service.unit.ts | 159 ++++++++++++++++++ .../sdk/chargebee/charge-bee.service.ts | 53 ++++++ src/providers/sdk/stripe/stripe.service.ts | 47 ++++++ src/types.ts | 23 +++ 4 files changed, 282 insertions(+) create mode 100644 src/__tests__/unit/invoice-pdf.service.unit.ts diff --git a/src/__tests__/unit/invoice-pdf.service.unit.ts b/src/__tests__/unit/invoice-pdf.service.unit.ts new file mode 100644 index 0000000..cbd140c --- /dev/null +++ b/src/__tests__/unit/invoice-pdf.service.unit.ts @@ -0,0 +1,159 @@ +import {expect, sinon} from '@loopback/testlab'; +import chargebee from 'chargebee'; +import {ChargeBeeService} from '../../providers/sdk/chargebee/charge-bee.service'; +import {StripeService} from '../../providers/sdk/stripe/stripe.service'; +import {TInvoicePdf} from '../../types'; + +// ------------------------------------------------------------------------- +// ChargeBee Tests +// ------------------------------------------------------------------------- + +describe('ChargeBeeService - Invoice PDF Download', () => { + let service: ChargeBeeService; + let sandbox: sinon.SinonSandbox; + + /** + * Helper function to stub ChargeBee API calls. + * ChargeBee SDK uses a builder pattern: chargebee.resource.action(params).request() + * So each stub must return an object with a `.request` stub. + */ + function stubCb(returnValue: object) { + // NOSONAR + return { + request: sinon.stub().resolves(returnValue), + setIdempotencyKey: sinon.stub().returnsThis(), + headers: sinon.stub().returnsThis(), + }; + } + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Stub the global chargebee.configure to prevent side effects + sandbox.stub(chargebee, 'configure'); + + // Initialize service with test configuration + service = new ChargeBeeService({ + site: 'test-site', + apiKey: 'test-key', + }); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getInvoicePdf - Happy Path', () => { + it('returns PDF URL for a valid invoice', async () => { + // Stub the chargebee.invoice.pdf() call + const pdfStub = sandbox.stub(chargebee.invoice, 'pdf').returns( + stubCb({ + download: { + download_url: 'https://test.chargebee.com/invoice/inv_123/pdf', + expires_at: 1735689600, // 2024-12-31 + }, + }), + ); + + // Call the method + const result: TInvoicePdf = await service.getInvoicePdf('inv_123'); + + // Verify the result + expect(result.invoiceId).to.equal('inv_123'); + expect(result.pdfUrl).to.equal( + 'https://test.chargebee.com/invoice/inv_123/pdf', + ); + expect(result.expiresAt).to.equal(1735689600); + expect(result.generatedAt).to.be.type('number'); + expect(result.generatedAt).to.be.greaterThan(0); + + // Verify the API was called correctly + sinon.assert.calledOnce(pdfStub); + sinon.assert.calledWith(pdfStub, 'inv_123'); + }); + + it('returns PDF URL with current timestamp', async () => { + sandbox.stub(chargebee.invoice, 'pdf').returns( + stubCb({ + download: { + download_url: 'https://test.chargebee.com/invoice/inv_456/pdf', + expires_at: 1735689600, + }, + }), + ); + + const before = Math.floor(Date.now() / 1000); + const result = await service.getInvoicePdf('inv_456'); + const after = Math.floor(Date.now() / 1000); + + expect(result.generatedAt).to.be.greaterThanOrEqual(before); + expect(result.generatedAt).to.be.lessThanOrEqual(after); + }); + }); + + describe('getInvoicePdf - Error Cases', () => { + it('throws error when PDF URL is not available', async () => { + // Stub to return empty download object + sandbox.stub(chargebee.invoice, 'pdf').returns( + stubCb({ + download: {}, + }), + ); + + await expect(service.getInvoicePdf('inv_123')).to.be.rejectedWith( + 'PDF URL not available for invoice inv_123. The invoice may be in an invalid state.', + ); + }); + + it('throws error when download object is missing', async () => { + // Stub to return result without download + sandbox.stub(chargebee.invoice, 'pdf').returns(stubCb({})); + + await expect(service.getInvoicePdf('inv_123')).to.be.rejectedWith( + 'PDF URL not available for invoice inv_123. The invoice may be in an invalid state.', + ); + }); + }); +}); + +// ------------------------------------------------------------------------- +// Stripe Tests +// ------------------------------------------------------------------------- + +describe('StripeService - Invoice PDF Download', () => { + let service: StripeService; + let sandbox: sinon.SinonSandbox; + + beforeEach(() => { + sandbox = sinon.createSandbox(); + + // Initialize service with test configuration + service = new StripeService({secretKey: 'sk_test_123'}); + }); + + afterEach(() => { + sandbox.restore(); + }); + + describe('getInvoicePdf - Error Cases', () => { + it('throws error for non-existent invoice', async () => { + // Mock Stripe error + sandbox + .stub(service['stripe'].invoices, 'retrieve') + .rejects({code: 'resource_missing'}); + + await expect(service.getInvoicePdf('in_nonexistent')).to.be.rejectedWith( + 'Invoice not found: in_nonexistent', + ); + }); + + it('throws error for other Stripe errors', async () => { + // Mock generic Stripe error + sandbox + .stub(service['stripe'].invoices, 'retrieve') + .rejects({code: 'api_error', message: 'Something went wrong'}); + + await expect(service.getInvoicePdf('in_error')).to.be.rejected(); + }); + }); +}); diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index 3d68ceb..78d4f65 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -4,6 +4,7 @@ import {inject} from '@loopback/core'; import chargebee from 'chargebee'; import { RecurringInterval, + TInvoicePdf, TInvoicePrice, TPrice, TProduct, @@ -614,4 +615,56 @@ export class ChargeBeeService implements IChargeBeeService { throw new Error(JSON.stringify(error)); } } + + /** + * Retrieves the PDF download URL for a ChargeBee invoice. + * + * ChargeBee uses the `invoice.pdf()` API to generate a temporary download URL + * for the invoice PDF. The URL is typically valid for a limited time. + * + * @param invoiceId - The ChargeBee invoice ID + * @returns Object containing the PDF URL, expiry time, and generation timestamp + * @throws Error if the invoice doesn't exist or PDF cannot be generated + */ + async getInvoicePdf(invoiceId: string): Promise { + try { + // Call ChargeBee's invoice.pdf() to generate the PDF URL + const result = await chargebee.invoice.pdf(invoiceId).request(); + + // Check if download URL is available + // Type assertion to handle ChargeBee SDK type limitations + const download = result.download as { + download_url?: string; + expires_at?: number; + }; + if (!download?.download_url) { + throw new Error( + `PDF URL not available for invoice ${invoiceId}. ` + + `The invoice may be in an invalid state.`, + ); + } + + // Return the PDF information + return { + invoiceId: invoiceId, + pdfUrl: download.download_url, + generatedAt: Math.floor(Date.now() / 1000), // Current timestamp in seconds + // ChargeBee provides expiry time for the download URL + expiresAt: download.expires_at, + }; + } catch (error) { + // Re-throw with better error message + const cbError = error as {api_error_code?: string; http_status?: number}; + const HTTP_NOT_FOUND = 404; + + if ( + cbError.api_error_code === 'resource_not_found' || + cbError.http_status === HTTP_NOT_FOUND + ) { + throw new Error(`Invoice not found: ${invoiceId}`); + } + + throw error; + } + } } diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index ecd2610..ec06191 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -1,10 +1,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ + import {inject} from '@loopback/core'; import Stripe from 'stripe'; import { CollectionMethod, RecurringInterval, TInvoice, + TInvoicePdf, TInvoicePrice, TPrice, TProduct, @@ -534,4 +536,49 @@ export class StripeService implements IStripeService { throw error; } } + + /** + * Retrieves the PDF download URL for a Stripe invoice. + * + * Stripe invoices have an `invoice_pdf` field that contains a temporary URL + * to download the PDF. This URL is typically valid for a limited time. + * + * Note: PDF URLs are only available for finalized invoices. Draft invoices + * will not have this field. + * + * @param invoiceId - The Stripe invoice ID + * @returns Object containing the PDF URL and generation timestamp + * @throws Error if the invoice doesn't exist or PDF URL is not available + */ + async getInvoicePdf(invoiceId: string): Promise { + try { + // Retrieve the invoice from Stripe + const invoice = await this.stripe.invoices.retrieve(invoiceId); + + // Check if PDF URL is available + if (!invoice.invoice_pdf) { + throw new Error( + `PDF URL not available for invoice ${invoiceId}. ` + + `The invoice may be in draft status or not finalized. ` + + `Only finalized invoices have PDF URLs.`, + ); + } + + // Return the PDF information + return { + invoiceId: invoice.id, + pdfUrl: invoice.invoice_pdf, + generatedAt: Math.floor(Date.now() / 1000), // Current timestamp in seconds + // Stripe PDF URLs have expiry but it's not exposed in the API response + // The URL is typically valid for a limited time (check Stripe docs) + }; + } catch (error) { + // Re-throw with better error message + const stripeError = error as {code?: string; message?: string}; + if (stripeError.code === 'resource_missing') { + throw new Error(`Invoice not found: ${invoiceId}`); + } + throw error; + } + } } diff --git a/src/types.ts b/src/types.ts index 7438b25..50cb47c 100644 --- a/src/types.ts +++ b/src/types.ts @@ -39,6 +39,7 @@ export interface IService { ): Promise; deleteInvoice(invoiceId: string): Promise; getPaymentStatus(invoiceId: string): Promise; + getInvoicePdf(invoiceId: string): Promise; } export interface IAdapter { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -244,6 +245,28 @@ export interface TInvoicePrice { amountExcludingTax: number; } +/** + * Represents a PDF download URL for an invoice. + * + * The PDF URL is typically temporary and expires after a certain period. + * The exact expiry duration depends on the billing provider. + */ +export interface TInvoicePdf { + /** The invoice ID */ + invoiceId: string; + /** The temporary download URL for the PDF */ + pdfUrl: string; + /** + * Timestamp (in seconds) when the URL expires, if provided by the provider. + * Some providers don't return expiry information. + */ + expiresAt?: number; + /** + * Timestamp (in seconds) when the PDF was generated/retrieved. + */ + generatedAt: number; +} + /** * Interface that any billing provider must implement to support the full * recurring-subscription lifecycle. From 41d4720fb465eebe3c52b734a3d3cd79f02d2c44 Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Thu, 7 May 2026 13:30:02 +0530 Subject: [PATCH 2/7] feat(chore): add invoice payment details and payment intent retrieval support add invoice payment details and payment intent retrieval support GH-27 --- .../sdk/chargebee/charge-bee.service.ts | 403 ++++++++++++++++++ .../chargebee/type/chargebee-config.type.ts | 80 ++++ src/providers/sdk/stripe/stripe.service.ts | 194 +++++++++ .../sdk/stripe/type/stripe-config.type.ts | 20 + src/types.ts | 126 ++++++ 5 files changed, 823 insertions(+) diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index 78d4f65..6bba110 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -5,7 +5,10 @@ import chargebee from 'chargebee'; import { RecurringInterval, TInvoicePdf, + TInvoicePaymentDetails, TInvoicePrice, + TPaymentIntent, + TPaymentMethod, TPrice, TProduct, TSubscriptionCreate, @@ -22,7 +25,11 @@ import { import {ChargeBeeBindings} from './key'; import { ChargeBeeConfig, + ChargebeeCard, ChargebeePeriodUnit, + ChargebeeCustomer, + ChargebeeInvoice, + ChargebeePaymentSource, IChargeBeeCustomer, IChargeBeeInvoice, IChargeBeePaymentSource, @@ -30,6 +37,12 @@ import { } from './type'; export class ChargeBeeService implements IChargeBeeService { + // Payment method default values + private static readonly DEFAULT_EXPIRY_MONTH = 12; + private static readonly DEFAULT_EXPIRY_YEAR = 2025; + private static readonly DEFAULT_FUNDING_TYPE = 'credit'; + private static readonly DEFAULT_CARD_BRAND = 'unknown'; + invoiceAdapter: InvoiceAdapter; customerAdapter: CustomerAdapter; paymentSource: PaymentSourceAdapter; @@ -667,4 +680,394 @@ export class ChargeBeeService implements IChargeBeeService { throw error; } } + + /** + * Retrieves payment method details associated with a ChargeBee invoice. + * + * This method retrieves the invoice, gets the payment source details from the + * customer, and returns comprehensive payment information. + * + * @param invoiceId - The ChargeBee invoice ID + * @returns Payment details including method, amount, and status + * @throws Error if invoice not found + */ + async getInvoicePaymentDetails( + invoiceId: string, + ): Promise { + try { + // Retrieve the invoice + const result = await chargebee.invoice.retrieve(invoiceId).request(); + const invoice = result.invoice as ChargebeeInvoice; + + // Get payment method based on invoice state + const paymentMethod = await this.getInvoicePaymentMethod( + invoice, + invoiceId, + ); + + // Build and return payment details + return this.buildPaymentDetails(invoice, invoiceId, paymentMethod); + } catch (error) { + const cbError = error as {api_error_code?: string; http_status?: number}; + if (cbError.api_error_code === 'resource_not_found') { + throw new Error(`Invoice not found: ${invoiceId}`); + } + throw error; + } + } + + /** + * Extracts customer ID from invoice. + * + * @param invoice - The ChargeBee invoice + * @param invoiceId - Invoice ID for error messages + * @returns Customer ID + * @throws Error if customer ID not found + */ + private getCustomerIdFromInvoice( + invoice: ChargebeeInvoice, + invoiceId: string, + ): string { + const customerId = + ((invoice as Record)['customer_id'] as string) || + invoice.customerId; + if (!customerId) { + throw new Error(`Customer ID not found for invoice ${invoiceId}`); + } + return customerId; + } + + /** + * Retrieves customer payment method for an invoice. + * + * @param invoice - The ChargeBee invoice + * @param invoiceId - Invoice ID for error messages + * @returns Payment method details + */ + private async getInvoicePaymentMethod( + invoice: ChargebeeInvoice, + invoiceId: string, + ): Promise { + // Check for linked payments first + if (invoice.linkedPayments && invoice.linkedPayments.length > 0) { + return this.handleLinkedPayment(invoice, invoiceId); + } + + // Fall back to customer's default payment method + return this.handleDefaultPaymentMethod(invoice, invoiceId); + } + + /** + * Handles payment method when invoice has linked payments. + * + * @param invoice - The ChargeBee invoice + * @param invoiceId - Invoice ID for error messages + * @returns Payment method details + */ + private async handleLinkedPayment( + invoice: ChargebeeInvoice, + invoiceId: string, + ): Promise { + if (!invoice.linkedPayments || invoice.linkedPayments.length === 0) { + throw new Error(`No linked payments found for invoice ${invoiceId}`); + } + + const payment = invoice.linkedPayments[0]; + + // Check if payment was applied + if (payment.appliedAt) { + return this.getCustomerPaymentMethod(invoice, invoiceId); + } + + // Payment not yet processed + return this.createPendingPaymentMethod(); + } + + /** + * Handles customer's default payment method. + * + * @param invoice - The ChargeBee invoice + * @param invoiceId - Invoice ID for error messages + * @returns Payment method details + */ + private async handleDefaultPaymentMethod( + invoice: ChargebeeInvoice, + invoiceId: string, + ): Promise { + return this.getCustomerPaymentMethod(invoice, invoiceId); + } + + /** + * Retrieves payment method from customer. + * + * @param invoice - The ChargeBee invoice + * @param invoiceId - Invoice ID for error messages + * @returns Payment method details + */ + private async getCustomerPaymentMethod( + invoice: ChargebeeInvoice, + invoiceId: string, + ): Promise { + const customerId = this.getCustomerIdFromInvoice(invoice, invoiceId); + const customerResult = await chargebee.customer + .retrieve(customerId) + .request(); + const customer = customerResult.customer as ChargebeeCustomer; + + if (customer.paymentSource) { + return this.adaptChargeBeePaymentSource(customer.paymentSource); + } + + return this.createUnknownPaymentMethod( + 'Payment method details not available', + ); + } + + /** + * Creates a pending payment method object. + * + * @returns Payment method with pending status + */ + private createPendingPaymentMethod(): TPaymentMethod { + return { + type: 'pending', + description: 'Payment not yet processed', + } as TPaymentMethod; + } + + /** + * Creates an unknown payment method object. + * + * @param description - Description for the unknown payment method + * @returns Payment method with unknown status + */ + private createUnknownPaymentMethod(description: string): TPaymentMethod { + return { + type: 'unknown', + description, + } as TPaymentMethod; + } + + /** + * Builds payment details response object. + * + * @param invoice - The ChargeBee invoice + * @param invoiceId - Invoice ID for fallback + * @param paymentMethod - Payment method details + * @returns Formatted payment details + */ + private buildPaymentDetails( + invoice: ChargebeeInvoice, + invoiceId: string, + paymentMethod: TPaymentMethod, + ): TInvoicePaymentDetails { + const id = invoice.invoiceId ?? invoiceId; + return { + invoiceId: id, + paymentMethod: paymentMethod, + paymentDate: invoice.paidAt + ? Math.floor(new Date(invoice.paidAt).getTime() / 1000) + : undefined, + amount: invoice.total, + currency: invoice.currencyCode ?? 'USD', + status: invoice.status ?? 'unknown', + transactionId: id, + description: `Payment for invoice ${id}`, + }; + } + + /** + * Retrieves a ChargeBee transaction by ID and returns it in PaymentIntent format. + * + * NOTE: This is a limited implementation that maps ChargeBee transactions to + * PaymentIntent format. ChargeBee's transaction model differs significantly from + * Stripe's PaymentIntent concept: + * + * - ChargeBee transactions represent actual payment attempts (not payment flow tracking) + * - Transactions are always tied to invoices/subscriptions (cannot be created independently) + * - No clientSecret for frontend payment completion + * - Limited real-time status tracking (only processing states, not payment flow states) + * - ChargeBee uses hosted payment pages instead of direct frontend integration + * + * @param paymentIntentId - The ChargeBee transaction ID + * @returns Payment intent details with ChargeBee transaction data mapped + * @throws Error if transaction not found + */ + async getPaymentIntent(paymentIntentId: string): Promise { + try { + // Retrieve the transaction from ChargeBee + const result = await chargebee.transaction + .retrieve(paymentIntentId) + .request(); + const transaction = result.transaction; + + // Get payment method details if payment_source_id is available + let paymentMethod: TPaymentMethod | undefined; + if (transaction.payment_source_id) { + try { + const paymentSourceResult = await chargebee.payment_source + .retrieve(transaction.payment_source_id) + .request(); + const paymentSource = + paymentSourceResult.payment_source as ChargebeePaymentSource; + paymentMethod = this.adaptChargeBeePaymentSource(paymentSource); + } catch (paymentSourceError) { + // If payment source not found, we'll continue without payment method details + console.info( + `Could not retrieve payment source ${transaction.payment_source_id}:`, + paymentSourceError, + ); + } + } + + // Map ChargeBee transaction to PaymentIntent format + return { + id: transaction.id, + amount: transaction.amount ?? 0, + currency: transaction.currency_code.toLowerCase(), + status: this.mapTransactionStatusToPaymentIntentStatus( + transaction.status ?? 'in_progress', + ), + created: transaction.date ?? 0, + customer: transaction.customer_id, + paymentMethod: paymentMethod, + description: `ChargeBee ${transaction.type} transaction`, + latestCharge: transaction.id, // In ChargeBee, transaction is the charge + clientSecret: undefined, // ChargeBee doesn't have client secret concept + amountCapturable: transaction.amount_capturable, + captureMethod: 'automatic', // ChargeBee default behavior + }; + } catch (error) { + const cbError = error as {api_error_code?: string; http_status?: number}; + const HTTP_NOT_FOUND = 404; + + if ( + cbError.api_error_code === 'resource_not_found' || + cbError.http_status === HTTP_NOT_FOUND + ) { + throw new Error(`Transaction not found: ${paymentIntentId}`); + } + + throw new Error(JSON.stringify(error)); + } + } + + /** + * Maps ChargeBee transaction status to PaymentIntent status. + * + * ChargeBee transaction statuses: in_progress, success, voided, failure, timeout, needs_attention, late_failure + * PaymentIntent statuses: requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, succeeded + * + * @param transactionStatus - ChargeBee transaction status + * @returns Corresponding PaymentIntent status + */ + private mapTransactionStatusToPaymentIntentStatus( + transactionStatus: string, + ): string { + const statusMap: Record = { + in_progress: 'processing', + success: 'succeeded', + voided: 'canceled', + failure: 'canceled', + timeout: 'canceled', + needs_attention: 'requires_action', + late_failure: 'canceled', + }; + return statusMap[transactionStatus] || 'requires_payment_method'; + } + + /** + * Adapts a ChargeBee payment source to the generic TPaymentMethod format. + */ + private adaptChargeBeePaymentSource( + source: ChargebeePaymentSource, + ): TPaymentMethod { + if (source.type === 'card' && source.card) { + return this.createCardPaymentMethod(source, source.card); + } + + return this.createGenericPaymentMethod(source); + } + + /** + * Creates a card payment method from ChargeBee payment source. + * + * @param source - The ChargeBee payment source + * @param card - The card details (guaranteed to be defined when called) + * @returns Card payment method + */ + private createCardPaymentMethod( + source: ChargebeePaymentSource, + card: ChargebeeCard, + ): TPaymentMethod { + return { + type: 'card', + id: source.id ?? '', + customer: source.customerId, + card: this.buildCardDetails(card), + }; + } + + /** + * Builds card details object from ChargeBee card data. + * + * @param card - ChargeBee card information + * @returns Formatted card details + */ + private buildCardDetails(card: ChargebeeCard): { + brand: string; + last4: string; + expMonth: number; + expYear: number; + funding: string; + } { + return { + brand: this.getCardBrand(card), + last4: card.last4 ?? '****', + expMonth: card.expiryMonth ?? ChargeBeeService.DEFAULT_EXPIRY_MONTH, + expYear: card.expiryYear ?? ChargeBeeService.DEFAULT_EXPIRY_YEAR, + funding: card.funding ?? ChargeBeeService.DEFAULT_FUNDING_TYPE, + }; + } + + /** + * Gets card brand from first six digits. + * + * @param card - ChargeBee card information + * @returns Card brand + */ + private getCardBrand(card: ChargebeeCard): string { + if (card.firstSixDigits) { + return this.detectCardBrand(card.firstSixDigits); + } + return ChargeBeeService.DEFAULT_CARD_BRAND; + } + + /** + * Creates a generic payment method for non-card payment sources. + * + * @param source - The ChargeBee payment source + * @returns Generic payment method + */ + private createGenericPaymentMethod( + source: ChargebeePaymentSource, + ): TPaymentMethod { + return { + type: source.type ?? 'unknown', + id: source.id ?? '', + customer: source.customerId, + }; + } + + /** + * Detects card brand from first six digits. + */ + private detectCardBrand(firstSix: string): string { + // Simple card brand detection + if (firstSix.startsWith('4')) return 'visa'; + if (firstSix.startsWith('5') || firstSix.startsWith('2')) + return 'mastercard'; + if (firstSix.startsWith('3')) return 'amex'; + return 'unknown'; + } } diff --git a/src/providers/sdk/chargebee/type/chargebee-config.type.ts b/src/providers/sdk/chargebee/type/chargebee-config.type.ts index 91f9829..fd4da2d 100644 --- a/src/providers/sdk/chargebee/type/chargebee-config.type.ts +++ b/src/providers/sdk/chargebee/type/chargebee-config.type.ts @@ -44,3 +44,83 @@ export type ChargebeePricingModel = | 'stairstep'; export type ChargebeePeriodUnit = 'day' | 'week' | 'month' | 'year'; + +/** + * ChargeBee linked payment object structure (flexible to handle API responses) + */ +export interface ChargebeeLinkedPayment { + id?: string; + customerId?: string; + invoiceId?: string; + appliedAt?: string; + amount?: number; + currencyCode?: string; + status?: string; + [key: string]: unknown; +} + +/** + * ChargeBee card object structure (flexible to handle API responses) + */ +export interface ChargebeeCard { + firstSixDigits?: string; + last4?: string; + expiryMonth?: number; + expiryYear?: number; + funding?: string; + [key: string]: unknown; +} + +/** + * ChargeBee payment source object structure (flexible to handle API responses) + */ +export interface ChargebeePaymentSource { + id?: string; + customerId?: string; + type?: string; + card?: ChargebeeCard; + [key: string]: unknown; +} + +/** + * ChargeBee customer object structure (flexible to handle API responses) + */ +export interface ChargebeeCustomer { + id?: string; + firstName?: string; + lastName?: string; + email?: string; + paymentSource?: ChargebeePaymentSource; + [key: string]: unknown; +} + +/** + * ChargeBee invoice object structure (flexible to handle API responses) + */ +export interface ChargebeeInvoice { + invoiceId?: string; + id?: string; + customerId?: string; + total?: number; + currencyCode?: string; + status?: string; + paidAt?: string; + linkedPayments?: ChargebeeLinkedPayment[]; + [key: string]: unknown; +} + +/** + * ChargeBee transaction object structure (flexible to handle API responses) + */ +export interface ChargebeeTransaction { + id?: string; + customerId?: string; + amount?: number; + currencyCode?: string; + status?: string; + date?: string | number; + description?: string; + metadata?: Record; + gatewayAccountId?: string; + [key: string]: unknown; +} diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index ec06191..59def0b 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -7,7 +7,10 @@ import { RecurringInterval, TInvoice, TInvoicePdf, + TInvoicePaymentDetails, TInvoicePrice, + TPaymentIntent, + TPaymentMethod, TPrice, TProduct, TSubscriptionCreate, @@ -28,8 +31,15 @@ import { IStripePaymentSource, IStripeService, StripeConfig, + StripeLegacySource, } from './type'; export class StripeService implements IStripeService { + // Payment method default values + private static readonly DEFAULT_EXPIRY_MONTH = 12; + private static readonly DEFAULT_EXPIRY_YEAR = 2025; + private static readonly DEFAULT_FUNDING_TYPE = 'credit'; + private static readonly DEFAULT_CARD_BRAND = 'unknown'; + /** * Stripe SDK instance. `protected` to allow subclasses (and test doubles) * to substitute the instance without re-opening the class. @@ -581,4 +591,188 @@ export class StripeService implements IStripeService { throw error; } } + + /** + * Retrieves payment method details associated with a Stripe invoice. + * + * This method retrieves the invoice, expands to get the charge and payment + * method details, and returns comprehensive payment information. + * + * @param invoiceId - The Stripe invoice ID + * @returns Payment details including method, amount, and status + * @throws Error if invoice not found or no payment available + */ + async getInvoicePaymentDetails( + invoiceId: string, + ): Promise { + try { + // Retrieve the invoice with expanded charge and payment method + const invoice = await this.stripe.invoices.retrieve(invoiceId, { + expand: ['charge', 'default_payment_method'], + }); + + // Check if invoice has a charge + if (!invoice.charge) { + throw new Error( + `No payment found for invoice ${invoiceId}. The invoice may not be paid yet.`, + ); + } + + const charge = invoice.charge as Stripe.Charge; + + // Get payment method details + let paymentMethod: TPaymentMethod; + + if (charge.payment_method) { + // Retrieve the payment method + const pm = await this.stripe.paymentMethods.retrieve( + charge.payment_method as string, + ); + paymentMethod = this.adaptPaymentMethod(pm); + } else if (charge.source) { + // Legacy source-based payment + const source = charge.source as unknown as StripeLegacySource; + paymentMethod = this.adaptSource(source); + } else { + throw new Error('No payment method information available'); + } + + return { + invoiceId: invoice.id, + paymentMethod: paymentMethod, + paymentDate: invoice.status_transitions?.paid_at ?? undefined, + amount: charge.amount, + currency: charge.currency, + status: charge.status, + transactionId: charge.id, + description: charge.description ?? undefined, + }; + } catch (error) { + const stripeError = error as {code?: string; message?: string}; + if (stripeError.code === 'resource_missing') { + throw new Error(`Invoice not found: ${invoiceId}`); + } + throw error; + } + } + + /** + * Retrieves a Stripe payment intent by ID. + * + * Payment intents represent the payment flow from initiation to completion. + * This method returns comprehensive payment tracking information. + * + * @param paymentIntentId - The Stripe payment intent ID + * @returns Payment intent details including status, amount, and method + * @throws Error if payment intent not found + */ + async getPaymentIntent(paymentIntentId: string): Promise { + try { + // Retrieve the payment intent with expanded payment method + const paymentIntent = await this.stripe.paymentIntents.retrieve( + paymentIntentId, + { + expand: ['payment_method', 'latest_charge'], + }, + ); + + // Adapt payment method if available + let paymentMethod: TPaymentMethod | undefined; + if (paymentIntent.payment_method) { + if (typeof paymentIntent.payment_method === 'string') { + // If it's just an ID, retrieve the full payment method + const pm = await this.stripe.paymentMethods.retrieve( + paymentIntent.payment_method, + ); + paymentMethod = this.adaptPaymentMethod(pm); + } else { + // Already expanded + paymentMethod = this.adaptPaymentMethod( + paymentIntent.payment_method as Stripe.PaymentMethod, + ); + } + } + + return { + id: paymentIntent.id, + amount: paymentIntent.amount, + currency: paymentIntent.currency, + status: paymentIntent.status, + created: paymentIntent.created, + customer: (paymentIntent.customer as string) ?? undefined, + paymentMethod: paymentMethod, + description: paymentIntent.description ?? undefined, + metadata: paymentIntent.metadata as Record, + latestCharge: (paymentIntent.latest_charge as string) ?? undefined, + clientSecret: paymentIntent.client_secret ?? undefined, + amountCapturable: paymentIntent.amount_capturable, + captureMethod: paymentIntent.capture_method, + }; + } catch (error) { + const stripeError = error as {code?: string; message?: string}; + if (stripeError.code === 'resource_missing') { + throw new Error(`Payment intent not found: ${paymentIntentId}`); + } + throw error; + } + } + + /** + * Adapts a Stripe PaymentMethod to the generic TPaymentMethod format. + */ + private adaptPaymentMethod(pm: Stripe.PaymentMethod): TPaymentMethod { + if (pm.type === 'card') { + return { + type: 'card', + id: pm.id, + customer: pm.customer as string, + card: { + brand: pm.card!.brand, + last4: pm.card!.last4, + expMonth: pm.card!.exp_month, + expYear: pm.card!.exp_year, + funding: pm.card!.funding, + country: pm.card!.country ?? undefined, + }, + }; + } + + // Handle other payment method types as needed + return { + type: pm.type, + id: pm.id, + customer: pm.customer as string, + }; + } + + /** + * Adapts a legacy Stripe Source to the generic TPaymentMethod format. + */ + private adaptSource(source: StripeLegacySource): TPaymentMethod { + if (source.type === 'card' && source.card) { + const card = source.card as { + brand: string; + last4: string; + expMonth: number; + expYear: number; + funding: string; + }; + return { + type: 'card', + id: source.id, + card: { + brand: card.brand || StripeService.DEFAULT_CARD_BRAND, + last4: card.last4 || '****', + expMonth: card.expMonth || StripeService.DEFAULT_EXPIRY_MONTH, + expYear: card.expYear || StripeService.DEFAULT_EXPIRY_YEAR, + funding: card.funding || StripeService.DEFAULT_FUNDING_TYPE, + }, + }; + } + + return { + type: source.type ?? 'unknown', + id: source.id, + }; + } } diff --git a/src/providers/sdk/stripe/type/stripe-config.type.ts b/src/providers/sdk/stripe/type/stripe-config.type.ts index 1d2f776..3d451fa 100644 --- a/src/providers/sdk/stripe/type/stripe-config.type.ts +++ b/src/providers/sdk/stripe/type/stripe-config.type.ts @@ -14,3 +14,23 @@ export interface StripeConfig { */ defaultPaymentBehavior?: Stripe.SubscriptionCreateParams.PaymentBehavior; } + +/** + * Stripe legacy Source object structure (for card-based payments) + * Made more flexible to handle Stripe's CustomerSource union type + */ +export type StripeLegacySource = { + id: string; + type?: string; + object?: string; + card?: { + brand?: string; + last4?: string; + expMonth?: number; + expYear?: number; + funding?: string; + }; + customer?: string; + metadata?: Record; + [key: string]: unknown; +}; diff --git a/src/types.ts b/src/types.ts index 50cb47c..c07b9f9 100644 --- a/src/types.ts +++ b/src/types.ts @@ -40,6 +40,8 @@ export interface IService { deleteInvoice(invoiceId: string): Promise; getPaymentStatus(invoiceId: string): Promise; getInvoicePdf(invoiceId: string): Promise; + getInvoicePaymentDetails(invoiceId: string): Promise; + getPaymentIntent(paymentIntentId: string): Promise; } export interface IAdapter { /* eslint-disable-next-line @typescript-eslint/no-explicit-any */ @@ -267,6 +269,130 @@ export interface TInvoicePdf { generatedAt: number; } +/** + * Represents payment method details (card, bank account, etc.) + */ +export interface TPaymentMethod { + /** Payment method type: card, bank_account, etc. */ + type: string; + + /** Card details (if type is card) */ + card?: { + /** Card brand: visa, mastercard, amex, etc. */ + brand: string; + /** Last 4 digits */ + last4: string; + /** Expiration month */ + expMonth: number; + /** Expiration year */ + expYear: number; + /** Funding type: credit, debit, prepaid, unknown */ + funding: string; + /** Country code */ + country?: string; + }; + + /** Bank account details (if type is bank_account) */ + bankAccount?: { + /** Bank name */ + bankName: string; + /** Last 4 digits */ + last4: string; + /** Routing number */ + routingNumber?: string; + /** Account type: checking, savings */ + accountType?: string; + }; + + /** Customer ID */ + customer?: string; + + /** Payment method ID at provider */ + id?: string; +} + +/** + * Complete payment details for an invoice + */ +export interface TInvoicePaymentDetails { + /** Invoice ID */ + invoiceId: string; + + /** Payment method information */ + paymentMethod: TPaymentMethod; + + /** Payment date (timestamp in seconds) */ + paymentDate?: number; + + /** Payment amount (in minor units) */ + amount?: number; + + /** Currency code */ + currency?: string; + + /** Payment status */ + status?: string; + + /** Transaction ID */ + transactionId?: string; + + /** Payment description */ + description?: string; +} + +/** + * Represents a payment intent for tracking payment flow + */ +export interface TPaymentIntent { + /** Payment intent ID */ + id: string; + + /** Payment amount (in minor units) */ + amount: number; + + /** Currency code */ + currency: string; + + /** + * Payment status: + * - requires_payment_method + * - requires_confirmation + * - requires_action + * - processing + * - requires_capture + * - canceled + * - succeeded + */ + status: string; + + /** Creation timestamp (seconds) */ + created: number; + + /** Customer ID */ + customer?: string; + + /** Payment method details */ + paymentMethod?: TPaymentMethod; + + /** Payment description */ + description?: string; + + /** Metadata key-value pairs */ + metadata?: Record; + + /** Latest charge ID */ + latestCharge?: string; + + /** Client secret for client-side confirmation */ + clientSecret?: string; + + /** Amount captured (if applicable) */ + amountCapturable?: number; + + /** Capture method: automatic or manual */ + captureMethod?: string; +} + /** * Interface that any billing provider must implement to support the full * recurring-subscription lifecycle. From 5cbb76a20dbd5d62cfa45f180cfc5765c93876be Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Fri, 8 May 2026 14:12:41 +0530 Subject: [PATCH 3/7] feat(chore): comments fix comments fix GH-27 --- src/providers/sdk/chargebee/adapter/index.ts | 1 + .../sdk/chargebee/adapter/invoice.adapter.ts | 64 ++++ .../adapter/payment-intent.adapter.ts | 189 +++++++++++ .../sdk/chargebee/charge-bee.service.ts | 304 ++---------------- src/providers/sdk/stripe/adapter/index.ts | 1 + .../sdk/stripe/adapter/invoice.adapter.ts | 46 ++- .../stripe/adapter/payment-intent.adapter.ts | 34 ++ .../stripe/adapter/payment-source.adapter.ts | 72 ++++- src/providers/sdk/stripe/stripe.service.ts | 121 ++----- 9 files changed, 456 insertions(+), 376 deletions(-) create mode 100644 src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts create mode 100644 src/providers/sdk/stripe/adapter/payment-intent.adapter.ts diff --git a/src/providers/sdk/chargebee/adapter/index.ts b/src/providers/sdk/chargebee/adapter/index.ts index ed2e797..d9a287c 100644 --- a/src/providers/sdk/chargebee/adapter/index.ts +++ b/src/providers/sdk/chargebee/adapter/index.ts @@ -1,4 +1,5 @@ export * from './customer.adapter'; export * from './invoice.adapter'; +export * from './payment-intent.adapter'; export * from './payment-source.adapter'; export * from './subscription.adapter'; diff --git a/src/providers/sdk/chargebee/adapter/invoice.adapter.ts b/src/providers/sdk/chargebee/adapter/invoice.adapter.ts index c1d55de..e1f9368 100644 --- a/src/providers/sdk/chargebee/adapter/invoice.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/invoice.adapter.ts @@ -1,3 +1,9 @@ +import { + TInvoicePdf, + TInvoicePaymentDetails, + TPaymentMethod, +} from '../../../../types'; +import {ChargebeeInvoice} from '../type'; import {AnyObject} from '@loopback/repository'; import {ICharge, IChargeBeeInvoice, IDiscount} from '../type'; export class InvoiceAdapter { @@ -38,4 +44,62 @@ export class InvoiceAdapter { }; return res; } + + /** + * Adapts a ChargeBee invoice download result to TInvoicePdf format. + * + * @param download - ChargeBee download object + * @param invoiceId - The invoice ID + * @returns TInvoicePdf - Invoice PDF information + */ + adaptToInvoicePdf( + download: Record, + invoiceId: string, + ): TInvoicePdf { + return { + invoiceId: invoiceId, + pdfUrl: String(download['download_url'] ?? ''), + generatedAt: Math.floor(Date.now() / 1000), // Current timestamp in seconds + expiresAt: download['expires_at'] as number | undefined, + }; + } + + /** + * Adapts ChargeBee invoice and payment method data to TInvoicePaymentDetails format. + * + * @param invoice - ChargeBee invoice object + * @param paymentMethod - Payment method details + * @returns TInvoicePaymentDetails - Payment details for the invoice + */ + adaptToPaymentDetails( + invoice: ChargebeeInvoice, + paymentMethod: TPaymentMethod, + ): TInvoicePaymentDetails { + const id = invoice.invoiceId ?? invoice.id ?? ''; + return { + invoiceId: id, + paymentMethod: paymentMethod, + paymentDate: invoice.paidAt + ? Math.floor(new Date(invoice.paidAt).getTime() / 1000) + : undefined, + amount: invoice.total ?? 0, + currency: invoice.currencyCode ?? 'USD', + status: invoice.status ?? 'unknown', + transactionId: id, + description: `Payment for invoice ${id}`, + }; + } + + /** + * Extracts customer ID from invoice. + * + * @param invoice - The ChargeBee invoice + * @returns Customer ID or empty string if not found + */ + getCustomerIdFromInvoice(invoice: ChargebeeInvoice): string { + const customerId = + ((invoice as Record)['customer_id'] as string) || + invoice.customerId; + return customerId ?? ''; + } } diff --git a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts new file mode 100644 index 0000000..a555a0c --- /dev/null +++ b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts @@ -0,0 +1,189 @@ +import {TPaymentIntent, TPaymentMethod} from '../../../../types'; +import { + ChargebeeCard, + ChargebeePaymentSource, + ChargebeeTransaction, +} from '../type'; + +// Payment method default values +const DEFAULT_EXPIRY_MONTH = 12; +const DEFAULT_EXPIRY_YEAR = 2025; +const DEFAULT_FUNDING_TYPE = 'credit'; +const DEFAULT_CARD_BRAND = 'unknown'; + +export class ChargebeePaymentIntentAdapter { + constructor() {} + + /** + * Adapts a ChargeBee transaction to the generic TPaymentIntent format. + * + * @param transaction - ChargeBee transaction object + * @param paymentMethod - Optional payment method details + * @returns TPaymentIntent - Normalized payment intent format + */ + adaptToModel( + transaction: ChargebeeTransaction, + paymentMethod?: TPaymentMethod, + ): TPaymentIntent { + const currencyCode = + ((transaction as Record)['currency_code'] as + | string + | undefined) ?? + transaction.currencyCode ?? + 'usd'; + const customerId = + ((transaction as Record)['customer_id'] as + | string + | undefined) ?? transaction.customerId; + const amountCapturable = (transaction as Record)[ + 'amount_capturable' + ] as number | undefined; + const transactionType = + ((transaction as Record)['type'] as + | string + | undefined) ?? 'transaction'; + + return { + id: transaction.id ?? '', + amount: transaction.amount ?? 0, + currency: String(currencyCode).toLowerCase(), + status: this.mapTransactionStatusToPaymentIntentStatus( + transaction.status ?? 'in_progress', + ), + created: + typeof transaction.date === 'string' + ? Math.floor(new Date(transaction.date).getTime() / 1000) + : (transaction.date ?? 0), + customer: customerId, + paymentMethod: paymentMethod, + description: `ChargeBee ${transactionType} transaction`, + latestCharge: transaction.id ?? '', // In ChargeBee, transaction is the charge + clientSecret: undefined, // ChargeBee doesn't have client secret concept + amountCapturable: amountCapturable, + captureMethod: 'automatic', // ChargeBee default behavior + }; + } + + /** + * Maps ChargeBee transaction status to PaymentIntent status. + * + * ChargeBee transaction statuses: in_progress, success, voided, failure, timeout, needs_attention, late_failure + * PaymentIntent statuses: requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, succeeded + * + * @param transactionStatus - ChargeBee transaction status + * @returns Corresponding PaymentIntent status + */ + private mapTransactionStatusToPaymentIntentStatus( + transactionStatus: string, + ): string { + switch (transactionStatus) { + case 'in_progress': + return 'processing'; + case 'success': + return 'succeeded'; + case 'voided': + return 'canceled'; + case 'failure': + return 'canceled'; + case 'timeout': + return 'canceled'; + case 'needs_attention': + return 'requires_action'; + case 'late_failure': + return 'canceled'; + default: + return 'requires_payment_method'; + } + } + + /** + * Adapts a ChargeBee payment source to the generic TPaymentMethod format. + */ + adaptPaymentSource(source: ChargebeePaymentSource): TPaymentMethod { + if (source.type === 'card' && source.card) { + return { + type: 'card', + id: source.id ?? '', + customer: source.customerId, + card: this.buildCardDetails(source.card), + }; + } + + return { + type: source.type ?? 'unknown', + id: source.id ?? '', + customer: source.customerId, + }; + } + + /** + * Builds card details object from ChargeBee card data. + * + * @param card - ChargeBee card information + * @returns Formatted card details + */ + private buildCardDetails(card: ChargebeeCard): { + brand: string; + last4: string; + expMonth: number; + expYear: number; + funding: string; + } { + return { + brand: this.getCardBrand(card), + last4: card.last4 ?? '****', + expMonth: card.expiryMonth ?? DEFAULT_EXPIRY_MONTH, + expYear: card.expiryYear ?? DEFAULT_EXPIRY_YEAR, + funding: card.funding ?? DEFAULT_FUNDING_TYPE, + }; + } + + /** + * Gets card brand from first six digits. + * + * @param card - ChargeBee card information + * @returns Card brand + */ + private getCardBrand(card: ChargebeeCard): string { + if (card.firstSixDigits) { + return this.detectCardBrand(card.firstSixDigits); + } + return DEFAULT_CARD_BRAND; + } + + /** + * Detects card brand from first six digits. + */ + private detectCardBrand(firstSix: string): string { + if (firstSix.startsWith('4')) return 'visa'; + if (firstSix.startsWith('5') || firstSix.startsWith('2')) + return 'mastercard'; + if (firstSix.startsWith('3')) return 'amex'; + return 'unknown'; + } + + /** + * Creates a pending payment method object. + * + * @returns Payment method with pending status + */ + createPendingPaymentMethod(): TPaymentMethod { + return { + type: 'pending', + description: 'Payment not yet processed', + } as TPaymentMethod; + } + + /** + * Creates an unknown payment method object. + * + * @param description - Description for the unknown payment method + * @returns Payment method with unknown status + */ + createUnknownPaymentMethod(description: string): TPaymentMethod { + return { + type: 'unknown', + description, + } as TPaymentMethod; + } +} diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index 6bba110..ffa673d 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -17,6 +17,7 @@ import { Transaction, } from '../../../types'; import { + ChargebeePaymentIntentAdapter, CustomerAdapter, InvoiceAdapter, PaymentSourceAdapter, @@ -25,7 +26,6 @@ import { import {ChargeBeeBindings} from './key'; import { ChargeBeeConfig, - ChargebeeCard, ChargebeePeriodUnit, ChargebeeCustomer, ChargebeeInvoice, @@ -37,16 +37,11 @@ import { } from './type'; export class ChargeBeeService implements IChargeBeeService { - // Payment method default values - private static readonly DEFAULT_EXPIRY_MONTH = 12; - private static readonly DEFAULT_EXPIRY_YEAR = 2025; - private static readonly DEFAULT_FUNDING_TYPE = 'credit'; - private static readonly DEFAULT_CARD_BRAND = 'unknown'; - invoiceAdapter: InvoiceAdapter; customerAdapter: CustomerAdapter; paymentSource: PaymentSourceAdapter; chargebeeSubscriptionAdapter: ChargebeeSubscriptionAdapter; + chargebeePaymentIntentAdapter: ChargebeePaymentIntentAdapter; constructor( @inject(ChargeBeeBindings.config, {optional: true}) private readonly chargeBeeConfig: ChargeBeeConfig, @@ -70,6 +65,7 @@ export class ChargeBeeService implements IChargeBeeService { this.customerAdapter = new CustomerAdapter(); this.paymentSource = new PaymentSourceAdapter(); this.chargebeeSubscriptionAdapter = new ChargebeeSubscriptionAdapter(); + this.chargebeePaymentIntentAdapter = new ChargebeePaymentIntentAdapter(); } async createCustomer( customerDto: IChargeBeeCustomer, @@ -657,14 +653,8 @@ export class ChargeBeeService implements IChargeBeeService { ); } - // Return the PDF information - return { - invoiceId: invoiceId, - pdfUrl: download.download_url, - generatedAt: Math.floor(Date.now() / 1000), // Current timestamp in seconds - // ChargeBee provides expiry time for the download URL - expiresAt: download.expires_at, - }; + // Return the PDF information using adapter + return this.invoiceAdapter.adaptToInvoicePdf(download, invoiceId); } catch (error) { // Re-throw with better error message const cbError = error as {api_error_code?: string; http_status?: number}; @@ -705,8 +695,8 @@ export class ChargeBeeService implements IChargeBeeService { invoiceId, ); - // Build and return payment details - return this.buildPaymentDetails(invoice, invoiceId, paymentMethod); + // Build and return payment details using adapter + return this.invoiceAdapter.adaptToPaymentDetails(invoice, paymentMethod); } catch (error) { const cbError = error as {api_error_code?: string; http_status?: number}; if (cbError.api_error_code === 'resource_not_found') { @@ -716,27 +706,6 @@ export class ChargeBeeService implements IChargeBeeService { } } - /** - * Extracts customer ID from invoice. - * - * @param invoice - The ChargeBee invoice - * @param invoiceId - Invoice ID for error messages - * @returns Customer ID - * @throws Error if customer ID not found - */ - private getCustomerIdFromInvoice( - invoice: ChargebeeInvoice, - invoiceId: string, - ): string { - const customerId = - ((invoice as Record)['customer_id'] as string) || - invoice.customerId; - if (!customerId) { - throw new Error(`Customer ID not found for invoice ${invoiceId}`); - } - return customerId; - } - /** * Retrieves customer payment method for an invoice. * @@ -750,50 +719,18 @@ export class ChargeBeeService implements IChargeBeeService { ): Promise { // Check for linked payments first if (invoice.linkedPayments && invoice.linkedPayments.length > 0) { - return this.handleLinkedPayment(invoice, invoiceId); - } + const payment = invoice.linkedPayments[0]; - // Fall back to customer's default payment method - return this.handleDefaultPaymentMethod(invoice, invoiceId); - } - - /** - * Handles payment method when invoice has linked payments. - * - * @param invoice - The ChargeBee invoice - * @param invoiceId - Invoice ID for error messages - * @returns Payment method details - */ - private async handleLinkedPayment( - invoice: ChargebeeInvoice, - invoiceId: string, - ): Promise { - if (!invoice.linkedPayments || invoice.linkedPayments.length === 0) { - throw new Error(`No linked payments found for invoice ${invoiceId}`); - } - - const payment = invoice.linkedPayments[0]; + // Check if payment was applied + if (payment.appliedAt) { + return this.getCustomerPaymentMethod(invoice, invoiceId); + } - // Check if payment was applied - if (payment.appliedAt) { - return this.getCustomerPaymentMethod(invoice, invoiceId); + // Payment not yet processed + return this.chargebeePaymentIntentAdapter.createPendingPaymentMethod(); } - // Payment not yet processed - return this.createPendingPaymentMethod(); - } - - /** - * Handles customer's default payment method. - * - * @param invoice - The ChargeBee invoice - * @param invoiceId - Invoice ID for error messages - * @returns Payment method details - */ - private async handleDefaultPaymentMethod( - invoice: ChargebeeInvoice, - invoiceId: string, - ): Promise { + // Fall back to customer's default payment method return this.getCustomerPaymentMethod(invoice, invoiceId); } @@ -808,74 +745,27 @@ export class ChargeBeeService implements IChargeBeeService { invoice: ChargebeeInvoice, invoiceId: string, ): Promise { - const customerId = this.getCustomerIdFromInvoice(invoice, invoiceId); + const customerId = this.invoiceAdapter.getCustomerIdFromInvoice(invoice); + if (!customerId) { + throw new Error(`Customer ID not found for invoice ${invoiceId}`); + } + const customerResult = await chargebee.customer .retrieve(customerId) .request(); const customer = customerResult.customer as ChargebeeCustomer; if (customer.paymentSource) { - return this.adaptChargeBeePaymentSource(customer.paymentSource); + return this.chargebeePaymentIntentAdapter.adaptPaymentSource( + customer.paymentSource, + ); } - return this.createUnknownPaymentMethod( + return this.chargebeePaymentIntentAdapter.createUnknownPaymentMethod( 'Payment method details not available', ); } - /** - * Creates a pending payment method object. - * - * @returns Payment method with pending status - */ - private createPendingPaymentMethod(): TPaymentMethod { - return { - type: 'pending', - description: 'Payment not yet processed', - } as TPaymentMethod; - } - - /** - * Creates an unknown payment method object. - * - * @param description - Description for the unknown payment method - * @returns Payment method with unknown status - */ - private createUnknownPaymentMethod(description: string): TPaymentMethod { - return { - type: 'unknown', - description, - } as TPaymentMethod; - } - - /** - * Builds payment details response object. - * - * @param invoice - The ChargeBee invoice - * @param invoiceId - Invoice ID for fallback - * @param paymentMethod - Payment method details - * @returns Formatted payment details - */ - private buildPaymentDetails( - invoice: ChargebeeInvoice, - invoiceId: string, - paymentMethod: TPaymentMethod, - ): TInvoicePaymentDetails { - const id = invoice.invoiceId ?? invoiceId; - return { - invoiceId: id, - paymentMethod: paymentMethod, - paymentDate: invoice.paidAt - ? Math.floor(new Date(invoice.paidAt).getTime() / 1000) - : undefined, - amount: invoice.total, - currency: invoice.currencyCode ?? 'USD', - status: invoice.status ?? 'unknown', - transactionId: id, - description: `Payment for invoice ${id}`, - }; - } - /** * Retrieves a ChargeBee transaction by ID and returns it in PaymentIntent format. * @@ -910,7 +800,10 @@ export class ChargeBeeService implements IChargeBeeService { .request(); const paymentSource = paymentSourceResult.payment_source as ChargebeePaymentSource; - paymentMethod = this.adaptChargeBeePaymentSource(paymentSource); + paymentMethod = + this.chargebeePaymentIntentAdapter.adaptPaymentSource( + paymentSource, + ); } catch (paymentSourceError) { // If payment source not found, we'll continue without payment method details console.info( @@ -920,23 +813,11 @@ export class ChargeBeeService implements IChargeBeeService { } } - // Map ChargeBee transaction to PaymentIntent format - return { - id: transaction.id, - amount: transaction.amount ?? 0, - currency: transaction.currency_code.toLowerCase(), - status: this.mapTransactionStatusToPaymentIntentStatus( - transaction.status ?? 'in_progress', - ), - created: transaction.date ?? 0, - customer: transaction.customer_id, - paymentMethod: paymentMethod, - description: `ChargeBee ${transaction.type} transaction`, - latestCharge: transaction.id, // In ChargeBee, transaction is the charge - clientSecret: undefined, // ChargeBee doesn't have client secret concept - amountCapturable: transaction.amount_capturable, - captureMethod: 'automatic', // ChargeBee default behavior - }; + // Return using adapter + return this.chargebeePaymentIntentAdapter.adaptToModel( + transaction, + paymentMethod, + ); } catch (error) { const cbError = error as {api_error_code?: string; http_status?: number}; const HTTP_NOT_FOUND = 404; @@ -951,123 +832,4 @@ export class ChargeBeeService implements IChargeBeeService { throw new Error(JSON.stringify(error)); } } - - /** - * Maps ChargeBee transaction status to PaymentIntent status. - * - * ChargeBee transaction statuses: in_progress, success, voided, failure, timeout, needs_attention, late_failure - * PaymentIntent statuses: requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, canceled, succeeded - * - * @param transactionStatus - ChargeBee transaction status - * @returns Corresponding PaymentIntent status - */ - private mapTransactionStatusToPaymentIntentStatus( - transactionStatus: string, - ): string { - const statusMap: Record = { - in_progress: 'processing', - success: 'succeeded', - voided: 'canceled', - failure: 'canceled', - timeout: 'canceled', - needs_attention: 'requires_action', - late_failure: 'canceled', - }; - return statusMap[transactionStatus] || 'requires_payment_method'; - } - - /** - * Adapts a ChargeBee payment source to the generic TPaymentMethod format. - */ - private adaptChargeBeePaymentSource( - source: ChargebeePaymentSource, - ): TPaymentMethod { - if (source.type === 'card' && source.card) { - return this.createCardPaymentMethod(source, source.card); - } - - return this.createGenericPaymentMethod(source); - } - - /** - * Creates a card payment method from ChargeBee payment source. - * - * @param source - The ChargeBee payment source - * @param card - The card details (guaranteed to be defined when called) - * @returns Card payment method - */ - private createCardPaymentMethod( - source: ChargebeePaymentSource, - card: ChargebeeCard, - ): TPaymentMethod { - return { - type: 'card', - id: source.id ?? '', - customer: source.customerId, - card: this.buildCardDetails(card), - }; - } - - /** - * Builds card details object from ChargeBee card data. - * - * @param card - ChargeBee card information - * @returns Formatted card details - */ - private buildCardDetails(card: ChargebeeCard): { - brand: string; - last4: string; - expMonth: number; - expYear: number; - funding: string; - } { - return { - brand: this.getCardBrand(card), - last4: card.last4 ?? '****', - expMonth: card.expiryMonth ?? ChargeBeeService.DEFAULT_EXPIRY_MONTH, - expYear: card.expiryYear ?? ChargeBeeService.DEFAULT_EXPIRY_YEAR, - funding: card.funding ?? ChargeBeeService.DEFAULT_FUNDING_TYPE, - }; - } - - /** - * Gets card brand from first six digits. - * - * @param card - ChargeBee card information - * @returns Card brand - */ - private getCardBrand(card: ChargebeeCard): string { - if (card.firstSixDigits) { - return this.detectCardBrand(card.firstSixDigits); - } - return ChargeBeeService.DEFAULT_CARD_BRAND; - } - - /** - * Creates a generic payment method for non-card payment sources. - * - * @param source - The ChargeBee payment source - * @returns Generic payment method - */ - private createGenericPaymentMethod( - source: ChargebeePaymentSource, - ): TPaymentMethod { - return { - type: source.type ?? 'unknown', - id: source.id ?? '', - customer: source.customerId, - }; - } - - /** - * Detects card brand from first six digits. - */ - private detectCardBrand(firstSix: string): string { - // Simple card brand detection - if (firstSix.startsWith('4')) return 'visa'; - if (firstSix.startsWith('5') || firstSix.startsWith('2')) - return 'mastercard'; - if (firstSix.startsWith('3')) return 'amex'; - return 'unknown'; - } } diff --git a/src/providers/sdk/stripe/adapter/index.ts b/src/providers/sdk/stripe/adapter/index.ts index ed2e797..d9a287c 100644 --- a/src/providers/sdk/stripe/adapter/index.ts +++ b/src/providers/sdk/stripe/adapter/index.ts @@ -1,4 +1,5 @@ export * from './customer.adapter'; export * from './invoice.adapter'; +export * from './payment-intent.adapter'; export * from './payment-source.adapter'; export * from './subscription.adapter'; diff --git a/src/providers/sdk/stripe/adapter/invoice.adapter.ts b/src/providers/sdk/stripe/adapter/invoice.adapter.ts index 97d19c9..845dd0b 100644 --- a/src/providers/sdk/stripe/adapter/invoice.adapter.ts +++ b/src/providers/sdk/stripe/adapter/invoice.adapter.ts @@ -1,6 +1,12 @@ /* eslint-disable @typescript-eslint/naming-convention */ +import Stripe from 'stripe'; import {AnyObject} from '@loopback/repository'; -import {IAdapter} from '../../../../types'; +import { + IAdapter, + TInvoicePdf, + TInvoicePaymentDetails, + TPaymentMethod, +} from '../../../../types'; import {IStripeInvoice} from '../type'; export class StripeInvoiceAdapter implements IAdapter { constructor() {} @@ -69,4 +75,42 @@ export class StripeInvoiceAdapter implements IAdapter { auto_advance: data.options?.autoAdvance ?? false, }; } + + /** + * Adapts a Stripe Invoice to TInvoicePdf format. + * + * @param invoice - Stripe Invoice object + * @returns TInvoicePdf - Invoice PDF information + */ + adaptToInvoicePdf(invoice: Stripe.Invoice): TInvoicePdf { + return { + invoiceId: invoice.id, + pdfUrl: invoice.invoice_pdf ?? '', + generatedAt: Math.floor(Date.now() / 1000), + }; + } + + /** + * Adapts Stripe invoice and charge data to TInvoicePaymentDetails format. + * + * @param invoice - Stripe Invoice object + * @param paymentMethod - Payment method details + * @returns TInvoicePaymentDetails - Payment details for the invoice + */ + adaptToPaymentDetails( + invoice: Stripe.Invoice, + paymentMethod: TPaymentMethod, + ): TInvoicePaymentDetails { + const charge = invoice.charge as Stripe.Charge; + return { + invoiceId: invoice.id, + paymentMethod: paymentMethod, + paymentDate: invoice.status_transitions?.paid_at ?? undefined, + amount: charge.amount, + currency: charge.currency, + status: charge.status, + transactionId: charge.id, + description: charge.description ?? undefined, + }; + } } diff --git a/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts b/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts new file mode 100644 index 0000000..2059d62 --- /dev/null +++ b/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts @@ -0,0 +1,34 @@ +import Stripe from 'stripe'; +import {TPaymentIntent, TPaymentMethod} from '../../../../types'; + +export class StripePaymentIntentAdapter { + constructor() {} + + /** + * Adapts a Stripe PaymentIntent to the generic TPaymentIntent format. + * + * @param paymentIntent - Stripe PaymentIntent object + * @param paymentMethod - Optional payment method details + * @returns TPaymentIntent - Normalized payment intent format + */ + adaptToModel( + paymentIntent: Stripe.PaymentIntent, + paymentMethod?: TPaymentMethod, + ): TPaymentIntent { + return { + id: paymentIntent.id, + amount: paymentIntent.amount, + currency: paymentIntent.currency, + status: paymentIntent.status, + created: paymentIntent.created, + customer: (paymentIntent.customer as string) ?? undefined, + paymentMethod: paymentMethod, + description: paymentIntent.description ?? undefined, + metadata: paymentIntent.metadata as Record, + latestCharge: (paymentIntent.latest_charge as string) ?? undefined, + clientSecret: paymentIntent.client_secret ?? undefined, + amountCapturable: paymentIntent.amount_capturable, + captureMethod: paymentIntent.capture_method, + }; + } +} diff --git a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts index 508284c..d7c1d76 100644 --- a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts +++ b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts @@ -1,6 +1,15 @@ +import Stripe from 'stripe'; import {AnyObject} from '@loopback/repository'; +import {TPaymentMethod} from '../../../../types'; import {IAdapter} from '../../../../types'; -import {IStripePaymentSource} from '../type'; +import {IStripePaymentSource, StripeLegacySource} from '../type'; + +// Payment method default values +const DEFAULT_EXPIRY_MONTH = 12; +const DEFAULT_EXPIRY_YEAR = 2025; +const DEFAULT_FUNDING_TYPE = 'credit'; +const DEFAULT_CARD_BRAND = 'unknown'; + export class StripePaymentAdapter implements IAdapter { constructor() {} @@ -22,7 +31,66 @@ export class StripePaymentAdapter implements IAdapter { }, }; } - adaptFromModel(data: IStripePaymentSource): AnyObject { + adaptFromModel(_data: IStripePaymentSource): AnyObject { return {}; // This is intentional } + + /** + * Adapts a Stripe PaymentMethod to the generic TPaymentMethod format. + */ + adaptPaymentMethod(pm: Stripe.PaymentMethod): TPaymentMethod { + if (pm.type === 'card') { + return { + type: 'card', + id: pm.id, + customer: pm.customer as string, + card: { + brand: pm.card!.brand, + last4: pm.card!.last4, + expMonth: pm.card!.exp_month, + expYear: pm.card!.exp_year, + funding: pm.card!.funding, + country: pm.card!.country ?? undefined, + }, + }; + } + + // Handle other payment method types as needed + return { + type: pm.type, + id: pm.id, + customer: pm.customer as string, + }; + } + + /** + * Adapts a legacy Stripe Source to the generic TPaymentMethod format. + */ + adaptSource(source: StripeLegacySource): TPaymentMethod { + if (source.type === 'card' && source.card) { + const card = source.card as { + brand: string; + last4: string; + expMonth: number; + expYear: number; + funding: string; + }; + return { + type: 'card', + id: source.id, + card: { + brand: card.brand || DEFAULT_CARD_BRAND, + last4: card.last4 || '****', + expMonth: card.expMonth || DEFAULT_EXPIRY_MONTH, + expYear: card.expYear || DEFAULT_EXPIRY_YEAR, + funding: card.funding || DEFAULT_FUNDING_TYPE, + }, + }; + } + + return { + type: source.type ?? 'unknown', + id: source.id, + }; + } } diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index 59def0b..6d27e45 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -22,6 +22,7 @@ import { StripeCustomerAdapter, StripeInvoiceAdapter, StripePaymentAdapter, + StripePaymentIntentAdapter, StripeSubscriptionAdapter, } from './adapter'; import {StripeBindings} from './key'; @@ -34,12 +35,6 @@ import { StripeLegacySource, } from './type'; export class StripeService implements IStripeService { - // Payment method default values - private static readonly DEFAULT_EXPIRY_MONTH = 12; - private static readonly DEFAULT_EXPIRY_YEAR = 2025; - private static readonly DEFAULT_FUNDING_TYPE = 'credit'; - private static readonly DEFAULT_CARD_BRAND = 'unknown'; - /** * Stripe SDK instance. `protected` to allow subclasses (and test doubles) * to substitute the instance without re-opening the class. @@ -49,6 +44,7 @@ export class StripeService implements IStripeService { stripeInvoiceAdapter: StripeInvoiceAdapter; stripePaymentAdapter: StripePaymentAdapter; stripeSubscriptionAdapter: StripeSubscriptionAdapter; + stripePaymentIntentAdapter: StripePaymentIntentAdapter; constructor( @inject(StripeBindings.config, {optional: true}) @@ -61,6 +57,7 @@ export class StripeService implements IStripeService { this.stripeInvoiceAdapter = new StripeInvoiceAdapter(); this.stripePaymentAdapter = new StripePaymentAdapter(); this.stripeSubscriptionAdapter = new StripeSubscriptionAdapter(); + this.stripePaymentIntentAdapter = new StripePaymentIntentAdapter(); } async createCustomer(customerDto: IStripeCustomer): Promise { @@ -574,14 +571,8 @@ export class StripeService implements IStripeService { ); } - // Return the PDF information - return { - invoiceId: invoice.id, - pdfUrl: invoice.invoice_pdf, - generatedAt: Math.floor(Date.now() / 1000), // Current timestamp in seconds - // Stripe PDF URLs have expiry but it's not exposed in the API response - // The URL is typically valid for a limited time (check Stripe docs) - }; + // Return the PDF information using adapter + return this.stripeInvoiceAdapter.adaptToInvoicePdf(invoice); } catch (error) { // Re-throw with better error message const stripeError = error as {code?: string; message?: string}; @@ -628,25 +619,20 @@ export class StripeService implements IStripeService { const pm = await this.stripe.paymentMethods.retrieve( charge.payment_method as string, ); - paymentMethod = this.adaptPaymentMethod(pm); + paymentMethod = this.stripePaymentAdapter.adaptPaymentMethod(pm); } else if (charge.source) { // Legacy source-based payment const source = charge.source as unknown as StripeLegacySource; - paymentMethod = this.adaptSource(source); + paymentMethod = this.stripePaymentAdapter.adaptSource(source); } else { throw new Error('No payment method information available'); } - return { - invoiceId: invoice.id, - paymentMethod: paymentMethod, - paymentDate: invoice.status_transitions?.paid_at ?? undefined, - amount: charge.amount, - currency: charge.currency, - status: charge.status, - transactionId: charge.id, - description: charge.description ?? undefined, - }; + // Return using adapter + return this.stripeInvoiceAdapter.adaptToPaymentDetails( + invoice, + paymentMethod, + ); } catch (error) { const stripeError = error as {code?: string; message?: string}; if (stripeError.code === 'resource_missing') { @@ -684,30 +670,20 @@ export class StripeService implements IStripeService { const pm = await this.stripe.paymentMethods.retrieve( paymentIntent.payment_method, ); - paymentMethod = this.adaptPaymentMethod(pm); + paymentMethod = this.stripePaymentAdapter.adaptPaymentMethod(pm); } else { // Already expanded - paymentMethod = this.adaptPaymentMethod( + paymentMethod = this.stripePaymentAdapter.adaptPaymentMethod( paymentIntent.payment_method as Stripe.PaymentMethod, ); } } - return { - id: paymentIntent.id, - amount: paymentIntent.amount, - currency: paymentIntent.currency, - status: paymentIntent.status, - created: paymentIntent.created, - customer: (paymentIntent.customer as string) ?? undefined, - paymentMethod: paymentMethod, - description: paymentIntent.description ?? undefined, - metadata: paymentIntent.metadata as Record, - latestCharge: (paymentIntent.latest_charge as string) ?? undefined, - clientSecret: paymentIntent.client_secret ?? undefined, - amountCapturable: paymentIntent.amount_capturable, - captureMethod: paymentIntent.capture_method, - }; + // Return using adapter + return this.stripePaymentIntentAdapter.adaptToModel( + paymentIntent, + paymentMethod, + ); } catch (error) { const stripeError = error as {code?: string; message?: string}; if (stripeError.code === 'resource_missing') { @@ -716,63 +692,4 @@ export class StripeService implements IStripeService { throw error; } } - - /** - * Adapts a Stripe PaymentMethod to the generic TPaymentMethod format. - */ - private adaptPaymentMethod(pm: Stripe.PaymentMethod): TPaymentMethod { - if (pm.type === 'card') { - return { - type: 'card', - id: pm.id, - customer: pm.customer as string, - card: { - brand: pm.card!.brand, - last4: pm.card!.last4, - expMonth: pm.card!.exp_month, - expYear: pm.card!.exp_year, - funding: pm.card!.funding, - country: pm.card!.country ?? undefined, - }, - }; - } - - // Handle other payment method types as needed - return { - type: pm.type, - id: pm.id, - customer: pm.customer as string, - }; - } - - /** - * Adapts a legacy Stripe Source to the generic TPaymentMethod format. - */ - private adaptSource(source: StripeLegacySource): TPaymentMethod { - if (source.type === 'card' && source.card) { - const card = source.card as { - brand: string; - last4: string; - expMonth: number; - expYear: number; - funding: string; - }; - return { - type: 'card', - id: source.id, - card: { - brand: card.brand || StripeService.DEFAULT_CARD_BRAND, - last4: card.last4 || '****', - expMonth: card.expMonth || StripeService.DEFAULT_EXPIRY_MONTH, - expYear: card.expYear || StripeService.DEFAULT_EXPIRY_YEAR, - funding: card.funding || StripeService.DEFAULT_FUNDING_TYPE, - }, - }; - } - - return { - type: source.type ?? 'unknown', - id: source.id, - }; - } } From 800a205beecfab49898259ca6db9785990c9864d Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Fri, 8 May 2026 15:48:13 +0530 Subject: [PATCH 4/7] feat(chore): sonar fix sonar fix GH-27 --- .../sdk/chargebee/adapter/invoice.adapter.ts | 7 +- .../adapter/payment-intent.adapter.ts | 70 +++++++++++++------ .../stripe/adapter/payment-source.adapter.ts | 3 +- 3 files changed, 53 insertions(+), 27 deletions(-) diff --git a/src/providers/sdk/chargebee/adapter/invoice.adapter.ts b/src/providers/sdk/chargebee/adapter/invoice.adapter.ts index e1f9368..e43e0b7 100644 --- a/src/providers/sdk/chargebee/adapter/invoice.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/invoice.adapter.ts @@ -3,9 +3,8 @@ import { TInvoicePaymentDetails, TPaymentMethod, } from '../../../../types'; -import {ChargebeeInvoice} from '../type'; +import {ChargebeeInvoice, ICharge, IChargeBeeInvoice, IDiscount} from '../type'; import {AnyObject} from '@loopback/repository'; -import {ICharge, IChargeBeeInvoice, IDiscount} from '../type'; export class InvoiceAdapter { constructor() {} @@ -56,9 +55,11 @@ export class InvoiceAdapter { download: Record, invoiceId: string, ): TInvoicePdf { + const downloadUrl = download['download_url']; + const pdfUrl = typeof downloadUrl === 'string' ? downloadUrl : ''; return { invoiceId: invoiceId, - pdfUrl: String(download['download_url'] ?? ''), + pdfUrl: pdfUrl, generatedAt: Math.floor(Date.now() / 1000), // Current timestamp in seconds expiresAt: download['expires_at'] as number | undefined, }; diff --git a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts index a555a0c..70e7a56 100644 --- a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts @@ -25,35 +25,20 @@ export class ChargebeePaymentIntentAdapter { transaction: ChargebeeTransaction, paymentMethod?: TPaymentMethod, ): TPaymentIntent { - const currencyCode = - ((transaction as Record)['currency_code'] as - | string - | undefined) ?? - transaction.currencyCode ?? - 'usd'; - const customerId = - ((transaction as Record)['customer_id'] as - | string - | undefined) ?? transaction.customerId; - const amountCapturable = (transaction as Record)[ - 'amount_capturable' - ] as number | undefined; - const transactionType = - ((transaction as Record)['type'] as - | string - | undefined) ?? 'transaction'; + const currencyCode = this.extractCurrencyCode(transaction); + const customerId = this.extractCustomerId(transaction); + const amountCapturable = this.extractAmountCapturable(transaction); + const transactionType = this.extractTransactionType(transaction); + const created = this.extractCreatedTimestamp(transaction); return { id: transaction.id ?? '', amount: transaction.amount ?? 0, - currency: String(currencyCode).toLowerCase(), + currency: currencyCode.toLowerCase(), status: this.mapTransactionStatusToPaymentIntentStatus( transaction.status ?? 'in_progress', ), - created: - typeof transaction.date === 'string' - ? Math.floor(new Date(transaction.date).getTime() / 1000) - : (transaction.date ?? 0), + created: created, customer: customerId, paymentMethod: paymentMethod, description: `ChargeBee ${transactionType} transaction`, @@ -64,6 +49,47 @@ export class ChargebeePaymentIntentAdapter { }; } + private extractCurrencyCode(transaction: ChargebeeTransaction): string { + const currencyCode = + ((transaction as Record)['currency_code'] as + | string + | undefined) ?? transaction.currencyCode; + return currencyCode ?? 'usd'; + } + + private extractCustomerId( + transaction: ChargebeeTransaction, + ): string | undefined { + return ( + ((transaction as Record)['customer_id'] as + | string + | undefined) ?? transaction.customerId + ); + } + + private extractAmountCapturable( + transaction: ChargebeeTransaction, + ): number | undefined { + return (transaction as Record)['amount_capturable'] as + | number + | undefined; + } + + private extractTransactionType(transaction: ChargebeeTransaction): string { + return ( + ((transaction as Record)['type'] as + | string + | undefined) ?? 'transaction' + ); + } + + private extractCreatedTimestamp(transaction: ChargebeeTransaction): number { + if (typeof transaction.date === 'string') { + return Math.floor(new Date(transaction.date).getTime() / 1000); + } + return transaction.date ?? 0; + } + /** * Maps ChargeBee transaction status to PaymentIntent status. * diff --git a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts index d7c1d76..d22c07b 100644 --- a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts +++ b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts @@ -1,7 +1,6 @@ import Stripe from 'stripe'; import {AnyObject} from '@loopback/repository'; -import {TPaymentMethod} from '../../../../types'; -import {IAdapter} from '../../../../types'; +import {TPaymentMethod, IAdapter} from '../../../../types'; import {IStripePaymentSource, StripeLegacySource} from '../type'; // Payment method default values From 5cd374d6c86386d2937483c1a22a9f023efed037 Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Wed, 13 May 2026 14:44:16 +0530 Subject: [PATCH 5/7] feat(chore): fix comments fix comments GH-27 --- .../adapter/payment-intent.adapter.ts | 47 ++++++------ .../sdk/chargebee/charge-bee.service.ts | 4 +- .../chargebee/type/chargebee-config.type.ts | 20 +++++ .../stripe/adapter/payment-intent.adapter.ts | 4 +- .../stripe/adapter/payment-source.adapter.ts | 32 +++++--- src/providers/sdk/stripe/stripe.service.ts | 4 +- .../sdk/stripe/type/stripe-config.type.ts | 21 ++++++ src/types.ts | 73 ++++++++++++------- 8 files changed, 143 insertions(+), 62 deletions(-) diff --git a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts index 70e7a56..3bb113e 100644 --- a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts @@ -1,18 +1,23 @@ -import {TPaymentIntent, TPaymentMethod} from '../../../../types'; +import {PaymentStatus, TPaymentIntent, TPaymentMethod} from '../../../../types'; import { ChargebeeCard, ChargebeePaymentSource, ChargebeeTransaction, + ChargebeeCardDefaults, } from '../type'; -// Payment method default values -const DEFAULT_EXPIRY_MONTH = 12; -const DEFAULT_EXPIRY_YEAR = 2025; -const DEFAULT_FUNDING_TYPE = 'credit'; -const DEFAULT_CARD_BRAND = 'unknown'; - export class ChargebeePaymentIntentAdapter { - constructor() {} + private readonly cardDefaults: Required; + + constructor(cardDefaults?: ChargebeeCardDefaults) { + const currentYear = new Date().getFullYear(); + this.cardDefaults = { + defaultExpiryMonth: cardDefaults?.defaultExpiryMonth ?? 12, + defaultExpiryYear: cardDefaults?.defaultExpiryYear ?? currentYear, + defaultFundingType: cardDefaults?.defaultFundingType ?? 'credit', + defaultCardBrand: cardDefaults?.defaultCardBrand ?? 'unknown', + }; + } /** * Adapts a ChargeBee transaction to the generic TPaymentIntent format. @@ -101,24 +106,24 @@ export class ChargebeePaymentIntentAdapter { */ private mapTransactionStatusToPaymentIntentStatus( transactionStatus: string, - ): string { + ): PaymentStatus { switch (transactionStatus) { case 'in_progress': - return 'processing'; + return PaymentStatus.PROCESSING; case 'success': - return 'succeeded'; + return PaymentStatus.SUCCEEDED; case 'voided': - return 'canceled'; + return PaymentStatus.CANCELED; case 'failure': - return 'canceled'; + return PaymentStatus.CANCELED; case 'timeout': - return 'canceled'; + return PaymentStatus.CANCELED; case 'needs_attention': - return 'requires_action'; + return PaymentStatus.REQUIRES_ACTION; case 'late_failure': - return 'canceled'; + return PaymentStatus.CANCELED; default: - return 'requires_payment_method'; + return PaymentStatus.REQUIRES_PAYMENT_METHOD; } } @@ -158,9 +163,9 @@ export class ChargebeePaymentIntentAdapter { return { brand: this.getCardBrand(card), last4: card.last4 ?? '****', - expMonth: card.expiryMonth ?? DEFAULT_EXPIRY_MONTH, - expYear: card.expiryYear ?? DEFAULT_EXPIRY_YEAR, - funding: card.funding ?? DEFAULT_FUNDING_TYPE, + expMonth: card.expiryMonth ?? this.cardDefaults.defaultExpiryMonth, + expYear: card.expiryYear ?? this.cardDefaults.defaultExpiryYear, + funding: card.funding ?? this.cardDefaults.defaultFundingType, }; } @@ -174,7 +179,7 @@ export class ChargebeePaymentIntentAdapter { if (card.firstSixDigits) { return this.detectCardBrand(card.firstSixDigits); } - return DEFAULT_CARD_BRAND; + return this.cardDefaults.defaultCardBrand; } /** diff --git a/src/providers/sdk/chargebee/charge-bee.service.ts b/src/providers/sdk/chargebee/charge-bee.service.ts index ffa673d..069f8b5 100644 --- a/src/providers/sdk/chargebee/charge-bee.service.ts +++ b/src/providers/sdk/chargebee/charge-bee.service.ts @@ -65,7 +65,9 @@ export class ChargeBeeService implements IChargeBeeService { this.customerAdapter = new CustomerAdapter(); this.paymentSource = new PaymentSourceAdapter(); this.chargebeeSubscriptionAdapter = new ChargebeeSubscriptionAdapter(); - this.chargebeePaymentIntentAdapter = new ChargebeePaymentIntentAdapter(); + this.chargebeePaymentIntentAdapter = new ChargebeePaymentIntentAdapter( + chargeBeeConfig.cardDefaults, + ); } async createCustomer( customerDto: IChargeBeeCustomer, diff --git a/src/providers/sdk/chargebee/type/chargebee-config.type.ts b/src/providers/sdk/chargebee/type/chargebee-config.type.ts index fd4da2d..74a0118 100644 --- a/src/providers/sdk/chargebee/type/chargebee-config.type.ts +++ b/src/providers/sdk/chargebee/type/chargebee-config.type.ts @@ -1,3 +1,18 @@ +/** + * Card default values for when card data is missing from the provider response. + * These should only be used as fallbacks when payment providers return incomplete data. + */ +export interface ChargebeeCardDefaults { + /** Default expiry month (1-12). Defaults to 12. */ + defaultExpiryMonth?: number; + /** Default expiry year. Defaults to current year. */ + defaultExpiryYear?: number; + /** Default funding type. Defaults to 'credit'. */ + defaultFundingType?: string; + /** Default card brand. Defaults to 'unknown'. */ + defaultCardBrand?: string; +} + /** * Configuration for the Chargebee billing provider. * @@ -34,6 +49,11 @@ export interface ChargeBeeConfig { * Must be one of the reason codes configured on your Chargebee site. */ defaultCancelReasonCode?: string; + /** + * Card default values for fallback when provider returns incomplete card data. + * These should rarely be needed with valid Chargebee responses. + */ + cardDefaults?: ChargebeeCardDefaults; } export type ChargebeePricingModel = diff --git a/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts b/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts index 2059d62..8041d4f 100644 --- a/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts +++ b/src/providers/sdk/stripe/adapter/payment-intent.adapter.ts @@ -1,5 +1,5 @@ import Stripe from 'stripe'; -import {TPaymentIntent, TPaymentMethod} from '../../../../types'; +import {PaymentStatus, TPaymentIntent, TPaymentMethod} from '../../../../types'; export class StripePaymentIntentAdapter { constructor() {} @@ -19,7 +19,7 @@ export class StripePaymentIntentAdapter { id: paymentIntent.id, amount: paymentIntent.amount, currency: paymentIntent.currency, - status: paymentIntent.status, + status: paymentIntent.status as PaymentStatus, created: paymentIntent.created, customer: (paymentIntent.customer as string) ?? undefined, paymentMethod: paymentMethod, diff --git a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts index d22c07b..d34b685 100644 --- a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts +++ b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts @@ -1,16 +1,24 @@ import Stripe from 'stripe'; import {AnyObject} from '@loopback/repository'; import {TPaymentMethod, IAdapter} from '../../../../types'; -import {IStripePaymentSource, StripeLegacySource} from '../type'; - -// Payment method default values -const DEFAULT_EXPIRY_MONTH = 12; -const DEFAULT_EXPIRY_YEAR = 2025; -const DEFAULT_FUNDING_TYPE = 'credit'; -const DEFAULT_CARD_BRAND = 'unknown'; +import { + IStripePaymentSource, + StripeLegacySource, + StripeCardDefaults, +} from '../type'; export class StripePaymentAdapter implements IAdapter { - constructor() {} + private readonly cardDefaults: Required; + + constructor(cardDefaults?: StripeCardDefaults) { + const currentYear = new Date().getFullYear(); + this.cardDefaults = { + defaultExpiryMonth: cardDefaults?.defaultExpiryMonth ?? 12, + defaultExpiryYear: cardDefaults?.defaultExpiryYear ?? currentYear, + defaultFundingType: cardDefaults?.defaultFundingType ?? 'credit', + defaultCardBrand: cardDefaults?.defaultCardBrand ?? 'unknown', + }; + } adaptToModel(resp: AnyObject): IStripePaymentSource { return { @@ -78,11 +86,11 @@ export class StripePaymentAdapter implements IAdapter { type: 'card', id: source.id, card: { - brand: card.brand || DEFAULT_CARD_BRAND, + brand: card.brand || this.cardDefaults.defaultCardBrand, last4: card.last4 || '****', - expMonth: card.expMonth || DEFAULT_EXPIRY_MONTH, - expYear: card.expYear || DEFAULT_EXPIRY_YEAR, - funding: card.funding || DEFAULT_FUNDING_TYPE, + expMonth: card.expMonth || this.cardDefaults.defaultExpiryMonth, + expYear: card.expYear || this.cardDefaults.defaultExpiryYear, + funding: card.funding || this.cardDefaults.defaultFundingType, }, }; } diff --git a/src/providers/sdk/stripe/stripe.service.ts b/src/providers/sdk/stripe/stripe.service.ts index 6d27e45..e48bbe6 100644 --- a/src/providers/sdk/stripe/stripe.service.ts +++ b/src/providers/sdk/stripe/stripe.service.ts @@ -55,7 +55,9 @@ export class StripeService implements IStripeService { }); this.stripeCustomerAdapter = new StripeCustomerAdapter(); this.stripeInvoiceAdapter = new StripeInvoiceAdapter(); - this.stripePaymentAdapter = new StripePaymentAdapter(); + this.stripePaymentAdapter = new StripePaymentAdapter( + stripeConfig.cardDefaults, + ); this.stripeSubscriptionAdapter = new StripeSubscriptionAdapter(); this.stripePaymentIntentAdapter = new StripePaymentIntentAdapter(); } diff --git a/src/providers/sdk/stripe/type/stripe-config.type.ts b/src/providers/sdk/stripe/type/stripe-config.type.ts index 3d451fa..6e7b22d 100644 --- a/src/providers/sdk/stripe/type/stripe-config.type.ts +++ b/src/providers/sdk/stripe/type/stripe-config.type.ts @@ -1,4 +1,20 @@ import Stripe from 'stripe'; + +/** + * Card default values for when card data is missing from the provider response. + * These should only be used as fallbacks when payment providers return incomplete data. + */ +export interface StripeCardDefaults { + /** Default expiry month (1-12). Defaults to 12. */ + defaultExpiryMonth?: number; + /** Default expiry year. Defaults to current year. */ + defaultExpiryYear?: number; + /** Default funding type. Defaults to 'credit'. */ + defaultFundingType?: string; + /** Default card brand. Defaults to 'unknown'. */ + defaultCardBrand?: string; +} + export interface StripeConfig { secretKey: string; /** @@ -13,6 +29,11 @@ export interface StripeConfig { * @see https://stripe.com/docs/api/subscriptions/create#create_subscription-payment_behavior */ defaultPaymentBehavior?: Stripe.SubscriptionCreateParams.PaymentBehavior; + /** + * Card default values for fallback when provider returns incomplete card data. + * These should rarely be needed with valid Stripe responses. + */ + cardDefaults?: StripeCardDefaults; } /** diff --git a/src/types.ts b/src/types.ts index c07b9f9..24398c7 100644 --- a/src/types.ts +++ b/src/types.ts @@ -167,6 +167,19 @@ export enum ProrationBehavior { ALWAYS_INVOICE = 'always_invoice', } +/** + * Payment intent status values + */ +export enum PaymentStatus { + REQUIRES_PAYMENT_METHOD = 'requires_payment_method', + REQUIRES_CONFIRMATION = 'requires_confirmation', + REQUIRES_ACTION = 'requires_action', + PROCESSING = 'processing', + REQUIRES_CAPTURE = 'requires_capture', + CANCELED = 'canceled', + SUCCEEDED = 'succeeded', +} + /** * Parameters required to create a product in the billing provider. */ @@ -269,6 +282,38 @@ export interface TInvoicePdf { generatedAt: number; } +/** + * Card payment method details + */ +export interface TCard { + /** Card brand: visa, mastercard, amex, etc. */ + brand: string; + /** Last 4 digits */ + last4: string; + /** Expiration month */ + expMonth: number; + /** Expiration year */ + expYear: number; + /** Funding type: credit, debit, prepaid, unknown */ + funding: string; + /** Country code */ + country?: string; +} + +/** + * Bank account payment method details + */ +export interface TBankAccount { + /** Bank name */ + bankName: string; + /** Last 4 digits */ + last4: string; + /** Routing number */ + routingNumber?: string; + /** Account type: checking, savings */ + accountType?: string; +} + /** * Represents payment method details (card, bank account, etc.) */ @@ -277,32 +322,10 @@ export interface TPaymentMethod { type: string; /** Card details (if type is card) */ - card?: { - /** Card brand: visa, mastercard, amex, etc. */ - brand: string; - /** Last 4 digits */ - last4: string; - /** Expiration month */ - expMonth: number; - /** Expiration year */ - expYear: number; - /** Funding type: credit, debit, prepaid, unknown */ - funding: string; - /** Country code */ - country?: string; - }; + card?: TCard; /** Bank account details (if type is bank_account) */ - bankAccount?: { - /** Bank name */ - bankName: string; - /** Last 4 digits */ - last4: string; - /** Routing number */ - routingNumber?: string; - /** Account type: checking, savings */ - accountType?: string; - }; + bankAccount?: TBankAccount; /** Customer ID */ customer?: string; @@ -363,7 +386,7 @@ export interface TPaymentIntent { * - canceled * - succeeded */ - status: string; + status: PaymentStatus; /** Creation timestamp (seconds) */ created: number; From be2dc897665fc0d871838384a4cbe31234e65713 Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Wed, 13 May 2026 14:47:52 +0530 Subject: [PATCH 6/7] feat(chore): fix trivy issue fix trivy issue GH-27 --- package-lock.json | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/package-lock.json b/package-lock.json index 03c8f44..c86a8e6 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1130,24 +1130,24 @@ "license": "MIT" }, "node_modules/@loopback/build": { - "version": "12.0.11", - "resolved": "https://registry.npmjs.org/@loopback/build/-/build-12.0.11.tgz", - "integrity": "sha512-Qr3j1tF20YQ7W/0Ont8Fv5xAroXdkEwlOOn+tqv8K7D3aTGeSehMkCnrVpOWllbWmiMAdqtVJXlNNqFne8j3+w==", + "version": "12.0.12", + "resolved": "https://registry.npmjs.org/@loopback/build/-/build-12.0.12.tgz", + "integrity": "sha512-ufmzogGEHvKX4Phaab0du8W35vlIktLbiCHjMnqWrOzzzL35N61p4QnIsDV5ImfSEu4XmouZNLwwLuBuqlthWQ==", "dev": true, "license": "MIT", "dependencies": { "@loopback/eslint-config": "^16.0.1", "@types/mocha": "^10.0.10", - "@types/node": "^20.19.39", + "@types/node": "^20.19.40", "cross-spawn": "^7.0.6", "debug": "^4.4.3", "eslint": "^8.57.1", - "fs-extra": "^11.3.4", + "fs-extra": "^11.3.5", "glob": "^13.0.6", "lodash": "^4.18.1", "mocha": "^11.7.5", "nyc": "^18.0.0", - "prettier": "^3.8.2", + "prettier": "^3.8.3", "rimraf": "^5.0.10", "source-map-support": "^0.5.21", "typescript": "~5.2.2" @@ -1166,9 +1166,9 @@ } }, "node_modules/@loopback/build/node_modules/@types/node": { - "version": "20.19.39", - "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.39.tgz", - "integrity": "sha512-orrrD74MBUyK8jOAD/r0+lfa1I2MO6I+vAkmAWzMYbCcgrN4lCrmK52gRFQq/JRxfYPfonkr4b0jcY7Olqdqbw==", + "version": "20.19.41", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.19.41.tgz", + "integrity": "sha512-ECymXOukMnOoVkC2bb1Vc/w/836DXncOg5m8Xj1RH7xSHZJWNYY6Zh7EH477vcnD5egKNNfy2RpNOmuChhFPgQ==", "dev": true, "license": "MIT", "dependencies": { @@ -5886,9 +5886,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.2.tgz", + "integrity": "sha512-rVjf7ArG3LTk+FS6Yw81V1DLuZl1bRbNrev6Tmd/9RaroeeRRJhAt7jg/6YFxbvAQXUCavSoZhPPj6oOx+5KjQ==", "funding": [ { "type": "github", @@ -6309,9 +6309,9 @@ "license": "MIT" }, "node_modules/fs-extra": { - "version": "11.3.4", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.4.tgz", - "integrity": "sha512-CTXd6rk/M3/ULNQj8FBqBWHYBVYybQ3VPBw0xGKFe3tuH7ytT6ACnvzpIQ3UZtB8yvUKC2cXn1a+x+5EVQLovA==", + "version": "11.3.5", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.5.tgz", + "integrity": "sha512-eKpRKAovdpZtR1WopLHxlBWvAgPny3c4gX1G5Jhwmmw4XJj0ifSD5qB5TOo8hmA0wlRKDAOAhEE1yVPgs6Fgcg==", "dev": true, "license": "MIT", "dependencies": { @@ -14785,9 +14785,9 @@ } }, "node_modules/uuid": { - "version": "13.0.0", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.0.tgz", - "integrity": "sha512-XQegIaBTVUjSHliKqcnFqYypAd4S+WCYt5NIeRs6w/UAry7z8Y9j5ZwRRL4kzq9U3sD6v+85er9FvkEaBpji2w==", + "version": "13.0.2", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-13.0.2.tgz", + "integrity": "sha512-vzi9uRZ926x4XV73S/4qQaTwPXM2JBj6/6lI/byHH1jOpCzb0zDbfytgA9LcN/hzb2l7WQSQnxITOVx5un/wGw==", "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" From 08d88cf7bc0518a5212a2e78e6c416f6329c7ce1 Mon Sep 17 00:00:00 2001 From: Sourav Kashyap Date: Wed, 13 May 2026 14:51:57 +0530 Subject: [PATCH 7/7] feat(chore): fix sonar issue fix sonar issue GH-27 --- .../sdk/chargebee/adapter/payment-intent.adapter.ts | 13 ++++++++++--- .../sdk/stripe/adapter/payment-source.adapter.ts | 13 ++++++++++--- 2 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts index 3bb113e..0b14bd6 100644 --- a/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts +++ b/src/providers/sdk/chargebee/adapter/payment-intent.adapter.ts @@ -6,16 +6,23 @@ import { ChargebeeCardDefaults, } from '../type'; +/** Default fallback values for card details when config is not provided */ +const DEFAULT_EXPIRY_MONTH = 12; +const DEFAULT_FUNDING_TYPE = 'credit'; +const DEFAULT_CARD_BRAND = 'unknown'; + export class ChargebeePaymentIntentAdapter { private readonly cardDefaults: Required; constructor(cardDefaults?: ChargebeeCardDefaults) { const currentYear = new Date().getFullYear(); this.cardDefaults = { - defaultExpiryMonth: cardDefaults?.defaultExpiryMonth ?? 12, + defaultExpiryMonth: + cardDefaults?.defaultExpiryMonth ?? DEFAULT_EXPIRY_MONTH, defaultExpiryYear: cardDefaults?.defaultExpiryYear ?? currentYear, - defaultFundingType: cardDefaults?.defaultFundingType ?? 'credit', - defaultCardBrand: cardDefaults?.defaultCardBrand ?? 'unknown', + defaultFundingType: + cardDefaults?.defaultFundingType ?? DEFAULT_FUNDING_TYPE, + defaultCardBrand: cardDefaults?.defaultCardBrand ?? DEFAULT_CARD_BRAND, }; } diff --git a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts index d34b685..d05fb7a 100644 --- a/src/providers/sdk/stripe/adapter/payment-source.adapter.ts +++ b/src/providers/sdk/stripe/adapter/payment-source.adapter.ts @@ -7,16 +7,23 @@ import { StripeCardDefaults, } from '../type'; +/** Default fallback values for card details when config is not provided */ +const DEFAULT_EXPIRY_MONTH = 12; +const DEFAULT_FUNDING_TYPE = 'credit'; +const DEFAULT_CARD_BRAND = 'unknown'; + export class StripePaymentAdapter implements IAdapter { private readonly cardDefaults: Required; constructor(cardDefaults?: StripeCardDefaults) { const currentYear = new Date().getFullYear(); this.cardDefaults = { - defaultExpiryMonth: cardDefaults?.defaultExpiryMonth ?? 12, + defaultExpiryMonth: + cardDefaults?.defaultExpiryMonth ?? DEFAULT_EXPIRY_MONTH, defaultExpiryYear: cardDefaults?.defaultExpiryYear ?? currentYear, - defaultFundingType: cardDefaults?.defaultFundingType ?? 'credit', - defaultCardBrand: cardDefaults?.defaultCardBrand ?? 'unknown', + defaultFundingType: + cardDefaults?.defaultFundingType ?? DEFAULT_FUNDING_TYPE, + defaultCardBrand: cardDefaults?.defaultCardBrand ?? DEFAULT_CARD_BRAND, }; }