Skip to content

Commit

Permalink
Saved payment methods 💳 (#67)
Browse files Browse the repository at this point in the history
* feat: add saved payment method resources

* chore: linting

* chore: bump version

* fix: update card type value

* feat: add payment method notifications

* test: add payment method notification tests

* refactor: split payment method response types

* refactor: use shared card type

* feat: add generateAuthToken for customer

* chore: update changelog
  • Loading branch information
danbillson authored Nov 19, 2024
1 parent 3ca6241 commit 0827b17
Show file tree
Hide file tree
Showing 50 changed files with 721 additions and 4 deletions.
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,15 @@ When we make [non-breaking changes](https://developer.paddle.com/api-reference/a

This means when upgrading minor versions of the SDK, you may notice type errors. You can safely ignore these or fix by adding additional type guards.

## 1.10.0 - 2024-11-13

### Added

- Added `paymentMethods` resources
- Added `generateAuthToken` for customer

---

## 1.9.1 - 2024-10-16

### Fixed
Expand Down
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "@paddle/paddle-node-sdk",
"version": "1.9.1",
"version": "1.10.0",
"description": "A Node.js SDK that you can use to integrate Paddle Billing with applications written in server-side JavaScript.",
"main": "./dist/cjs/index.js",
"module": "./dist/esm/index.js",
Expand Down
42 changes: 42 additions & 0 deletions src/__tests__/mocks/notifications/payment-method-deleted.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,42 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

import { IPaymentMethodDeletedNotificationResponse } from '../../../notifications';
import { type IEventsResponse } from '../../../types';

export const PaymentMethodDeletedMock: IEventsResponse<IPaymentMethodDeletedNotificationResponse> = {
event_id: 'evt_01hwz6k64a210xcvsdbg3y4vmr',
event_type: 'payment_method.deleted',
occurred_at: '2024-05-03T12:24:18.826338Z',
notification_id: 'ntf_01hwz6k66fp6cxtxyt6551wv7z',
data: {
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
customer_id: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
address_id: 'add_01hv8gq3318ktkfengj2r75gfx',
deletion_reason: 'api',
type: 'card',
origin: 'saved_during_purchase',
saved_at: '2024-05-02T02:55:25.198953Z',
updated_at: '2024-05-03T12:24:18.826338Z',
},
};

export const PaymentMethodDeletedMockExpectation = {
data: {
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
addressId: 'add_01hv8gq3318ktkfengj2r75gfx',
deletionReason: 'api',
type: 'card',
origin: 'saved_during_purchase',
savedAt: '2024-05-02T02:55:25.198953Z',
updatedAt: '2024-05-03T12:24:18.826338Z',
},
eventId: 'evt_01hwz6k64a210xcvsdbg3y4vmr',
eventType: 'payment_method.deleted',
notificationId: 'ntf_01hwz6k66fp6cxtxyt6551wv7z',
occurredAt: '2024-05-03T12:24:18.826338Z',
};
39 changes: 39 additions & 0 deletions src/__tests__/mocks/notifications/payment-method-saved.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

import { type IEventsResponse, type IPaymentMethodResponse } from '../../../types';

export const PaymentMethodSavedMock: IEventsResponse<IPaymentMethodResponse> = {
event_id: 'evt_01hwvkmsge7bhq1a31s35784zt',
event_type: 'payment_method.saved',
occurred_at: '2024-05-02T02:55:25.198953Z',
notification_id: 'ntf_01hwvkmsknrgqw4z1598qw4ypt',
data: {
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
customer_id: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
address_id: 'add_01hv8gq3318ktkfengj2r75gfx',
type: 'card',
origin: 'saved_during_purchase',
saved_at: '2024-05-02T02:55:25.198953Z',
updated_at: '2024-05-02T02:55:25.198953Z',
},
};

export const PaymentMethodSavedMockExpectation = {
data: {
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
addressId: 'add_01hv8gq3318ktkfengj2r75gfx',
type: 'card',
origin: 'saved_during_purchase',
savedAt: '2024-05-02T02:55:25.198953Z',
updatedAt: '2024-05-02T02:55:25.198953Z',
},
eventId: 'evt_01hwvkmsge7bhq1a31s35784zt',
eventType: 'payment_method.saved',
notificationId: 'ntf_01hwvkmsknrgqw4z1598qw4ypt',
occurredAt: '2024-05-02T02:55:25.198953Z',
};
14 changes: 13 additions & 1 deletion src/__tests__/mocks/resources/customers.mock.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
* Changes may be overwritten as part of auto-generation.
*/

import { type ICreditBalanceResponse, ICustomerResponse } from '../../../types';
import { IAuthTokenResponse, type ICreditBalanceResponse, ICustomerResponse } from '../../../types';
import { Response, ResponsePaginated } from '../../../internal';

export const UpdateCustomerMock = {
Expand Down Expand Up @@ -36,6 +36,11 @@ export const CustomerMock: ICustomerResponse = {
import_meta: { external_id: '9b95b0b8-e10f-441a-862e-1936a6d818ab', imported_from: 'billing_platform' },
};

export const GenerateAuthTokenMock: IAuthTokenResponse = {
customer_auth_token: 'pca_01hwyzq8hmdwed5p4jc4hnv6bh_01hwwggymjn0yhhb2gr4p91276_6xaav4lydudt6bgmuefeaf2xnu3umegx',
expires_at: '2024-10-13T07:20:50.52Z',
};

export const CustomerCreditBalanceMock: ICreditBalanceResponse = {
balance: {
available: '200',
Expand Down Expand Up @@ -72,3 +77,10 @@ export const ListCustomerMockResponse: ResponsePaginated<ICustomerResponse> = {
},
},
};

export const GenerateAuthTokenMockResponse: Response<IAuthTokenResponse> = {
data: GenerateAuthTokenMock,
meta: {
request_id: '',
},
};
47 changes: 47 additions & 0 deletions src/__tests__/mocks/resources/payment-methods.mock.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,47 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

import { IPaymentMethodResponse } from '../../../types';
import { Response, ResponsePaginated } from '../../../internal';

export const PaymentMethodMock: IPaymentMethodResponse = {
id: 'paymtd_123',
customer_id: 'ctm_123',
address_id: 'add_123',
type: 'card',
card: {
type: 'visa',
last4: '1234',
expiry_month: 1,
expiry_year: 2025,

cardholder_name: 'Sam Miller',
},
paypal: null,
origin: 'saved_during_purchase',
saved_at: '2024-05-03T11:50:23.422Z',
updated_at: '2024-05-03T11:50:23.422Z',
};

export const PaymentMethodMockResponse: Response<IPaymentMethodResponse> = {
data: PaymentMethodMock,
meta: {
request_id: '',
},
};

export const ListPaymentMethodMockResponse: ResponsePaginated<IPaymentMethodResponse> = {
data: [PaymentMethodMock],
meta: {
request_id: '',
pagination: {
estimated_total: 10,
has_more: true,
next: '/customers/ctm_123/payment-methods?after=1',
per_page: 10,
},
},
};
10 changes: 10 additions & 0 deletions src/__tests__/notifications/notifications-parser.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,14 @@ import { CustomerUpdatedMock, CustomerUpdatedMockExpectation } from '../mocks/no
import { DiscountCreatedMock, DiscountCreatedMockExpectation } from '../mocks/notifications/discount-created.mock';
import { DiscountImportedMock, DiscountImportedMockExpectation } from '../mocks/notifications/discount-imported.mock';
import { DiscountUpdatedMock, DiscountUpdatedMockExpectation } from '../mocks/notifications/discount-updated.mock';
import {
PaymentMethodDeletedMock,
PaymentMethodDeletedMockExpectation,
} from '../mocks/notifications/payment-method-deleted.mock';
import {
PaymentMethodSavedMock,
PaymentMethodSavedMockExpectation,
} from '../mocks/notifications/payment-method-saved.mock';
import { PayoutCreatedMock, PayoutCreatedMockExpectation } from '../mocks/notifications/payout-created.mock';
import { PayoutPaidMock, PayoutPaidMockExpectation } from '../mocks/notifications/payout-paid.mock';
import { PriceCreatedMock, PriceCreatedMockExpectation } from '../mocks/notifications/price-created.mock';
Expand Down Expand Up @@ -113,6 +121,8 @@ describe('Notifications Parser', () => {
[DiscountCreatedMock.event_type, DiscountCreatedMock, DiscountCreatedMockExpectation],
[DiscountImportedMock.event_type, DiscountImportedMock, DiscountImportedMockExpectation],
[DiscountUpdatedMock.event_type, DiscountUpdatedMock, DiscountUpdatedMockExpectation],
[PaymentMethodDeletedMock.event_type, PaymentMethodDeletedMock, PaymentMethodDeletedMockExpectation],
[PaymentMethodSavedMock.event_type, PaymentMethodSavedMock, PaymentMethodSavedMockExpectation],
[PayoutCreatedMock.event_type, PayoutCreatedMock, PayoutCreatedMockExpectation],
[PayoutPaidMock.event_type, PayoutPaidMock, PayoutPaidMockExpectation],
[PriceCreatedMock.event_type, PriceCreatedMock, PriceCreatedMockExpectation],
Expand Down
13 changes: 13 additions & 0 deletions src/__tests__/resources/customers.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
CustomerCreditBalanceMockResponse,
CustomerMock,
CustomerMockResponse,
GenerateAuthTokenMockResponse,
ListCustomerMockResponse,
UpdateCustomerExpectation,
UpdateCustomerMock,
Expand Down Expand Up @@ -139,4 +140,16 @@ describe('CustomersResource', () => {
});
expect(customer).toBeDefined();
});

test('should generate an auth token for a customer', async () => {
const customerId = CustomerMock.id;
const paddleInstance = getPaddleTestClient();
paddleInstance.post = jest.fn().mockResolvedValue(GenerateAuthTokenMockResponse);

const customersResource = new CustomersResource(paddleInstance);
const authToken = (await customersResource.generateAuthToken(customerId)).customerAuthToken;

expect(paddleInstance.post).toBeCalledWith(`/customers/${customerId}/auth-token`, undefined);
expect(authToken).toBeDefined();
});
});
80 changes: 80 additions & 0 deletions src/__tests__/resources/payment-methods.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,80 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

import { PaymentMethodsResource, type ListCustomerPaymentMethodQueryParameters } from '../../resources';
import { getPaddleTestClient } from '../helpers/test-client';
import {
PaymentMethodMockResponse,
PaymentMethodMock,
ListPaymentMethodMockResponse,
} from '../mocks/resources/payment-methods.mock';

describe('PaymentMethodsResource', () => {
test('should return a list of payment methods', async () => {
const customerId = PaymentMethodMock.customer_id;

const paddleInstance = getPaddleTestClient();
paddleInstance.get = jest.fn().mockResolvedValue(ListPaymentMethodMockResponse);

const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
const paymentMethodCollection = paymentMethodsResource.list(customerId);

let paymentMethods = await paymentMethodCollection.next();
expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods?`);
expect(paymentMethods.length).toBe(1);

paymentMethods = await paymentMethodCollection.next();
expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods?after=1`);
expect(paymentMethods.length).toBe(1);
});

test('should accept query params and return a list of payment methods', async () => {
const customerId = PaymentMethodMock.customer_id;

const paddleInstance = getPaddleTestClient();
paddleInstance.get = jest.fn().mockResolvedValue(ListPaymentMethodMockResponse);
const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
const queryParams: ListCustomerPaymentMethodQueryParameters = {
after: '2',
addressId: ['adr_123'],
};

const paymentMethodCollection = paymentMethodsResource.list(customerId, queryParams);
let paymentMethods = await paymentMethodCollection.next();

expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods?after=2&address_id=adr_123`);
expect(paymentMethods.length).toBe(1);
});

test('should return a single payment method', async () => {
const paymentMethodId = PaymentMethodMock.id;
const customerId = PaymentMethodMock.customer_id;

const paddleInstance = getPaddleTestClient();
paddleInstance.get = jest.fn().mockResolvedValue(PaymentMethodMockResponse);

const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
const paymentMethod = await paymentMethodsResource.get(customerId, paymentMethodId);

expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods/${paymentMethodId}`);
expect(paymentMethod).toBeDefined();
expect(paymentMethod.id).toBe(paymentMethodId);
});

test('should delete an existing payment method', async () => {
const paymentMethodId = PaymentMethodMock.id;
const customerId = PaymentMethodMock.customer_id;

const paddleInstance = getPaddleTestClient();
paddleInstance.delete = jest.fn().mockResolvedValue(PaymentMethodMockResponse);

const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
const updatedPaymentMethod = await paymentMethodsResource.delete(customerId, paymentMethodId);

expect(paddleInstance.delete).toBeCalledWith(`/customers/${customerId}/payment-methods/${paymentMethodId}`);
expect(updatedPaymentMethod).toBeUndefined();
});
});
17 changes: 17 additions & 0 deletions src/entities/customer/auth-token.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

import { type IAuthTokenResponse } from '../../types';

export class AuthToken {
public readonly customerAuthToken: string;
public readonly expiresAt: string;

constructor(authToken: IAuthTokenResponse) {
this.customerAuthToken = authToken.customer_auth_token;
this.expiresAt = authToken.expires_at;
}
}
1 change: 1 addition & 0 deletions src/entities/customer/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
* Changes may be overwritten as part of auto-generation.
*/

export * from './auth-token';
export * from './customer-collection';
export * from './customer';
export * from './credit-balance';
Expand Down
1 change: 1 addition & 0 deletions src/entities/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ export * from './subscription';
export * from './address';
export * from './discount';
export * from './events';
export * from './payment-method';
export * from './payout';
export * from './event-types';
export * from './notification-settings';
Expand Down
8 changes: 8 additions & 0 deletions src/entities/payment-method/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

export * from './payment-method';
export * from './payment-method-collection';
15 changes: 15 additions & 0 deletions src/entities/payment-method/payment-method-collection.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
/**
* ! Autogenerated code !
* Do not make changes to this file.
* Changes may be overwritten as part of auto-generation.
*/

import { PaymentMethod } from '../../entities';
import { type IPaymentMethodResponse } from '../../types';
import { Collection } from '../../internal/base';

export class PaymentMethodCollection extends Collection<IPaymentMethodResponse, PaymentMethod> {
override fromJson(data: IPaymentMethodResponse): PaymentMethod {
return new PaymentMethod(data);
}
}
Loading

0 comments on commit 0827b17

Please sign in to comment.