Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 18 additions & 18 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

159 changes: 159 additions & 0 deletions src/__tests__/unit/invoice-pdf.service.unit.ts
Original file line number Diff line number Diff line change
@@ -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();
});
});
});
1 change: 1 addition & 0 deletions src/providers/sdk/chargebee/adapter/index.ts
Original file line number Diff line number Diff line change
@@ -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';
67 changes: 66 additions & 1 deletion src/providers/sdk/chargebee/adapter/invoice.adapter.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,10 @@
import {
TInvoicePdf,
TInvoicePaymentDetails,
TPaymentMethod,
} from '../../../../types';
import {ChargebeeInvoice, ICharge, IChargeBeeInvoice, IDiscount} from '../type';
import {AnyObject} from '@loopback/repository';
import {ICharge, IChargeBeeInvoice, IDiscount} from '../type';
export class InvoiceAdapter {
constructor() {}

Expand Down Expand Up @@ -38,4 +43,64 @@ 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<string, unknown>,
invoiceId: string,
): TInvoicePdf {
const downloadUrl = download['download_url'];
const pdfUrl = typeof downloadUrl === 'string' ? downloadUrl : '';
return {
invoiceId: invoiceId,
pdfUrl: pdfUrl,
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<string, unknown>)['customer_id'] as string) ||
invoice.customerId;
return customerId ?? '';
}
}
Loading
Loading