Skip to content

Commit 0827b17

Browse files
author
Dan Billson
authored
Saved payment methods 💳 (#67)
* 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
1 parent 3ca6241 commit 0827b17

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

50 files changed

+721
-4
lines changed

CHANGELOG.md

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,15 @@ When we make [non-breaking changes](https://developer.paddle.com/api-reference/a
1212

1313
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.
1414

15+
## 1.10.0 - 2024-11-13
16+
17+
### Added
18+
19+
- Added `paymentMethods` resources
20+
- Added `generateAuthToken` for customer
21+
22+
---
23+
1524
## 1.9.1 - 2024-10-16
1625

1726
### Fixed

package.json

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
{
22
"name": "@paddle/paddle-node-sdk",
3-
"version": "1.9.1",
3+
"version": "1.10.0",
44
"description": "A Node.js SDK that you can use to integrate Paddle Billing with applications written in server-side JavaScript.",
55
"main": "./dist/cjs/index.js",
66
"module": "./dist/esm/index.js",
Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
1+
/**
2+
* ! Autogenerated code !
3+
* Do not make changes to this file.
4+
* Changes may be overwritten as part of auto-generation.
5+
*/
6+
7+
import { IPaymentMethodDeletedNotificationResponse } from '../../../notifications';
8+
import { type IEventsResponse } from '../../../types';
9+
10+
export const PaymentMethodDeletedMock: IEventsResponse<IPaymentMethodDeletedNotificationResponse> = {
11+
event_id: 'evt_01hwz6k64a210xcvsdbg3y4vmr',
12+
event_type: 'payment_method.deleted',
13+
occurred_at: '2024-05-03T12:24:18.826338Z',
14+
notification_id: 'ntf_01hwz6k66fp6cxtxyt6551wv7z',
15+
data: {
16+
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
17+
customer_id: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
18+
address_id: 'add_01hv8gq3318ktkfengj2r75gfx',
19+
deletion_reason: 'api',
20+
type: 'card',
21+
origin: 'saved_during_purchase',
22+
saved_at: '2024-05-02T02:55:25.198953Z',
23+
updated_at: '2024-05-03T12:24:18.826338Z',
24+
},
25+
};
26+
27+
export const PaymentMethodDeletedMockExpectation = {
28+
data: {
29+
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
30+
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
31+
addressId: 'add_01hv8gq3318ktkfengj2r75gfx',
32+
deletionReason: 'api',
33+
type: 'card',
34+
origin: 'saved_during_purchase',
35+
savedAt: '2024-05-02T02:55:25.198953Z',
36+
updatedAt: '2024-05-03T12:24:18.826338Z',
37+
},
38+
eventId: 'evt_01hwz6k64a210xcvsdbg3y4vmr',
39+
eventType: 'payment_method.deleted',
40+
notificationId: 'ntf_01hwz6k66fp6cxtxyt6551wv7z',
41+
occurredAt: '2024-05-03T12:24:18.826338Z',
42+
};
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
/**
2+
* ! Autogenerated code !
3+
* Do not make changes to this file.
4+
* Changes may be overwritten as part of auto-generation.
5+
*/
6+
7+
import { type IEventsResponse, type IPaymentMethodResponse } from '../../../types';
8+
9+
export const PaymentMethodSavedMock: IEventsResponse<IPaymentMethodResponse> = {
10+
event_id: 'evt_01hwvkmsge7bhq1a31s35784zt',
11+
event_type: 'payment_method.saved',
12+
occurred_at: '2024-05-02T02:55:25.198953Z',
13+
notification_id: 'ntf_01hwvkmsknrgqw4z1598qw4ypt',
14+
data: {
15+
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
16+
customer_id: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
17+
address_id: 'add_01hv8gq3318ktkfengj2r75gfx',
18+
type: 'card',
19+
origin: 'saved_during_purchase',
20+
saved_at: '2024-05-02T02:55:25.198953Z',
21+
updated_at: '2024-05-02T02:55:25.198953Z',
22+
},
23+
};
24+
25+
export const PaymentMethodSavedMockExpectation = {
26+
data: {
27+
id: 'paymtd_01hs8zx6x377xfsfrt2bqsevbw',
28+
customerId: 'ctm_01hv6y1jedq4p1n0yqn5ba3ky4',
29+
addressId: 'add_01hv8gq3318ktkfengj2r75gfx',
30+
type: 'card',
31+
origin: 'saved_during_purchase',
32+
savedAt: '2024-05-02T02:55:25.198953Z',
33+
updatedAt: '2024-05-02T02:55:25.198953Z',
34+
},
35+
eventId: 'evt_01hwvkmsge7bhq1a31s35784zt',
36+
eventType: 'payment_method.saved',
37+
notificationId: 'ntf_01hwvkmsknrgqw4z1598qw4ypt',
38+
occurredAt: '2024-05-02T02:55:25.198953Z',
39+
};

src/__tests__/mocks/resources/customers.mock.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
* Changes may be overwritten as part of auto-generation.
55
*/
66

7-
import { type ICreditBalanceResponse, ICustomerResponse } from '../../../types';
7+
import { IAuthTokenResponse, type ICreditBalanceResponse, ICustomerResponse } from '../../../types';
88
import { Response, ResponsePaginated } from '../../../internal';
99

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

39+
export const GenerateAuthTokenMock: IAuthTokenResponse = {
40+
customer_auth_token: 'pca_01hwyzq8hmdwed5p4jc4hnv6bh_01hwwggymjn0yhhb2gr4p91276_6xaav4lydudt6bgmuefeaf2xnu3umegx',
41+
expires_at: '2024-10-13T07:20:50.52Z',
42+
};
43+
3944
export const CustomerCreditBalanceMock: ICreditBalanceResponse = {
4045
balance: {
4146
available: '200',
@@ -72,3 +77,10 @@ export const ListCustomerMockResponse: ResponsePaginated<ICustomerResponse> = {
7277
},
7378
},
7479
};
80+
81+
export const GenerateAuthTokenMockResponse: Response<IAuthTokenResponse> = {
82+
data: GenerateAuthTokenMock,
83+
meta: {
84+
request_id: '',
85+
},
86+
};
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
/**
2+
* ! Autogenerated code !
3+
* Do not make changes to this file.
4+
* Changes may be overwritten as part of auto-generation.
5+
*/
6+
7+
import { IPaymentMethodResponse } from '../../../types';
8+
import { Response, ResponsePaginated } from '../../../internal';
9+
10+
export const PaymentMethodMock: IPaymentMethodResponse = {
11+
id: 'paymtd_123',
12+
customer_id: 'ctm_123',
13+
address_id: 'add_123',
14+
type: 'card',
15+
card: {
16+
type: 'visa',
17+
last4: '1234',
18+
expiry_month: 1,
19+
expiry_year: 2025,
20+
21+
cardholder_name: 'Sam Miller',
22+
},
23+
paypal: null,
24+
origin: 'saved_during_purchase',
25+
saved_at: '2024-05-03T11:50:23.422Z',
26+
updated_at: '2024-05-03T11:50:23.422Z',
27+
};
28+
29+
export const PaymentMethodMockResponse: Response<IPaymentMethodResponse> = {
30+
data: PaymentMethodMock,
31+
meta: {
32+
request_id: '',
33+
},
34+
};
35+
36+
export const ListPaymentMethodMockResponse: ResponsePaginated<IPaymentMethodResponse> = {
37+
data: [PaymentMethodMock],
38+
meta: {
39+
request_id: '',
40+
pagination: {
41+
estimated_total: 10,
42+
has_more: true,
43+
next: '/customers/ctm_123/payment-methods?after=1',
44+
per_page: 10,
45+
},
46+
},
47+
};

src/__tests__/notifications/notifications-parser.test.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,14 @@ import { CustomerUpdatedMock, CustomerUpdatedMockExpectation } from '../mocks/no
1717
import { DiscountCreatedMock, DiscountCreatedMockExpectation } from '../mocks/notifications/discount-created.mock';
1818
import { DiscountImportedMock, DiscountImportedMockExpectation } from '../mocks/notifications/discount-imported.mock';
1919
import { DiscountUpdatedMock, DiscountUpdatedMockExpectation } from '../mocks/notifications/discount-updated.mock';
20+
import {
21+
PaymentMethodDeletedMock,
22+
PaymentMethodDeletedMockExpectation,
23+
} from '../mocks/notifications/payment-method-deleted.mock';
24+
import {
25+
PaymentMethodSavedMock,
26+
PaymentMethodSavedMockExpectation,
27+
} from '../mocks/notifications/payment-method-saved.mock';
2028
import { PayoutCreatedMock, PayoutCreatedMockExpectation } from '../mocks/notifications/payout-created.mock';
2129
import { PayoutPaidMock, PayoutPaidMockExpectation } from '../mocks/notifications/payout-paid.mock';
2230
import { PriceCreatedMock, PriceCreatedMockExpectation } from '../mocks/notifications/price-created.mock';
@@ -113,6 +121,8 @@ describe('Notifications Parser', () => {
113121
[DiscountCreatedMock.event_type, DiscountCreatedMock, DiscountCreatedMockExpectation],
114122
[DiscountImportedMock.event_type, DiscountImportedMock, DiscountImportedMockExpectation],
115123
[DiscountUpdatedMock.event_type, DiscountUpdatedMock, DiscountUpdatedMockExpectation],
124+
[PaymentMethodDeletedMock.event_type, PaymentMethodDeletedMock, PaymentMethodDeletedMockExpectation],
125+
[PaymentMethodSavedMock.event_type, PaymentMethodSavedMock, PaymentMethodSavedMockExpectation],
116126
[PayoutCreatedMock.event_type, PayoutCreatedMock, PayoutCreatedMockExpectation],
117127
[PayoutPaidMock.event_type, PayoutPaidMock, PayoutPaidMockExpectation],
118128
[PriceCreatedMock.event_type, PriceCreatedMock, PriceCreatedMockExpectation],

src/__tests__/resources/customers.test.ts

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
CustomerCreditBalanceMockResponse,
1616
CustomerMock,
1717
CustomerMockResponse,
18+
GenerateAuthTokenMockResponse,
1819
ListCustomerMockResponse,
1920
UpdateCustomerExpectation,
2021
UpdateCustomerMock,
@@ -139,4 +140,16 @@ describe('CustomersResource', () => {
139140
});
140141
expect(customer).toBeDefined();
141142
});
143+
144+
test('should generate an auth token for a customer', async () => {
145+
const customerId = CustomerMock.id;
146+
const paddleInstance = getPaddleTestClient();
147+
paddleInstance.post = jest.fn().mockResolvedValue(GenerateAuthTokenMockResponse);
148+
149+
const customersResource = new CustomersResource(paddleInstance);
150+
const authToken = (await customersResource.generateAuthToken(customerId)).customerAuthToken;
151+
152+
expect(paddleInstance.post).toBeCalledWith(`/customers/${customerId}/auth-token`, undefined);
153+
expect(authToken).toBeDefined();
154+
});
142155
});
Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
/**
2+
* ! Autogenerated code !
3+
* Do not make changes to this file.
4+
* Changes may be overwritten as part of auto-generation.
5+
*/
6+
7+
import { PaymentMethodsResource, type ListCustomerPaymentMethodQueryParameters } from '../../resources';
8+
import { getPaddleTestClient } from '../helpers/test-client';
9+
import {
10+
PaymentMethodMockResponse,
11+
PaymentMethodMock,
12+
ListPaymentMethodMockResponse,
13+
} from '../mocks/resources/payment-methods.mock';
14+
15+
describe('PaymentMethodsResource', () => {
16+
test('should return a list of payment methods', async () => {
17+
const customerId = PaymentMethodMock.customer_id;
18+
19+
const paddleInstance = getPaddleTestClient();
20+
paddleInstance.get = jest.fn().mockResolvedValue(ListPaymentMethodMockResponse);
21+
22+
const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
23+
const paymentMethodCollection = paymentMethodsResource.list(customerId);
24+
25+
let paymentMethods = await paymentMethodCollection.next();
26+
expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods?`);
27+
expect(paymentMethods.length).toBe(1);
28+
29+
paymentMethods = await paymentMethodCollection.next();
30+
expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods?after=1`);
31+
expect(paymentMethods.length).toBe(1);
32+
});
33+
34+
test('should accept query params and return a list of payment methods', async () => {
35+
const customerId = PaymentMethodMock.customer_id;
36+
37+
const paddleInstance = getPaddleTestClient();
38+
paddleInstance.get = jest.fn().mockResolvedValue(ListPaymentMethodMockResponse);
39+
const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
40+
const queryParams: ListCustomerPaymentMethodQueryParameters = {
41+
after: '2',
42+
addressId: ['adr_123'],
43+
};
44+
45+
const paymentMethodCollection = paymentMethodsResource.list(customerId, queryParams);
46+
let paymentMethods = await paymentMethodCollection.next();
47+
48+
expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods?after=2&address_id=adr_123`);
49+
expect(paymentMethods.length).toBe(1);
50+
});
51+
52+
test('should return a single payment method', async () => {
53+
const paymentMethodId = PaymentMethodMock.id;
54+
const customerId = PaymentMethodMock.customer_id;
55+
56+
const paddleInstance = getPaddleTestClient();
57+
paddleInstance.get = jest.fn().mockResolvedValue(PaymentMethodMockResponse);
58+
59+
const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
60+
const paymentMethod = await paymentMethodsResource.get(customerId, paymentMethodId);
61+
62+
expect(paddleInstance.get).toBeCalledWith(`/customers/${customerId}/payment-methods/${paymentMethodId}`);
63+
expect(paymentMethod).toBeDefined();
64+
expect(paymentMethod.id).toBe(paymentMethodId);
65+
});
66+
67+
test('should delete an existing payment method', async () => {
68+
const paymentMethodId = PaymentMethodMock.id;
69+
const customerId = PaymentMethodMock.customer_id;
70+
71+
const paddleInstance = getPaddleTestClient();
72+
paddleInstance.delete = jest.fn().mockResolvedValue(PaymentMethodMockResponse);
73+
74+
const paymentMethodsResource = new PaymentMethodsResource(paddleInstance);
75+
const updatedPaymentMethod = await paymentMethodsResource.delete(customerId, paymentMethodId);
76+
77+
expect(paddleInstance.delete).toBeCalledWith(`/customers/${customerId}/payment-methods/${paymentMethodId}`);
78+
expect(updatedPaymentMethod).toBeUndefined();
79+
});
80+
});
Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,17 @@
1+
/**
2+
* ! Autogenerated code !
3+
* Do not make changes to this file.
4+
* Changes may be overwritten as part of auto-generation.
5+
*/
6+
7+
import { type IAuthTokenResponse } from '../../types';
8+
9+
export class AuthToken {
10+
public readonly customerAuthToken: string;
11+
public readonly expiresAt: string;
12+
13+
constructor(authToken: IAuthTokenResponse) {
14+
this.customerAuthToken = authToken.customer_auth_token;
15+
this.expiresAt = authToken.expires_at;
16+
}
17+
}

0 commit comments

Comments
 (0)