diff --git a/CHANGELOG.md b/CHANGELOG.md index 8695f6e..7dcc498 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,6 +12,12 @@ 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. +## 2.1.3 - 2024-11-29 + +### Changed + +- `paddle.webhooks.unmarshal` will now return an event for unhandled event types instead of `null` this is only possible for legacy/no longer supported events or for new events that have not been added to the sdk yet + ## 2.1.2 - 2024-11-26 ### Fixed diff --git a/package.json b/package.json index c050320..eedaf66 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@paddle/paddle-node-sdk", - "version": "2.1.2", + "version": "2.1.3", "description": "A Node.js SDK that you can use to integrate Paddle Billing with applications written in server-side JavaScript.", "main": "dist/cjs/index.cjs.node.js", "module": "dist/esm/index.esm.node.js", @@ -68,5 +68,6 @@ "import": "./dist/esm/index.esm.node.js", "require": "./dist/cjs/index.cjs.node.js" } - } + }, + "dependencies": {} } diff --git a/src/__tests__/mocks/notifications/invoice-paid.mock.ts b/src/__tests__/mocks/notifications/invoice-paid.mock.ts new file mode 100644 index 0000000..6e5ca46 --- /dev/null +++ b/src/__tests__/mocks/notifications/invoice-paid.mock.ts @@ -0,0 +1,165 @@ +// Invoice paid is a legacy/unsupported event which is implicitly handled through GenericEvent + +import { IEventsResponse } from '../../../types/index.js'; + +export const InvoicePaidMock: IEventsResponse = { + event_id: 'evt_01jdw4vq5a26w8mpfc59mez047', + event_type: 'invoice.paid', + occurred_at: '2024-11-29T14:23:08.971054Z', + notification_id: 'ntf_01h90nmerv7vrn93f97j5v72p7', + data: { + id: 'inv_01jdw4vk9fr1n6smpbhykm6ha9', + items: [ + { + price: { + product_id: 'pro_01gv5dvjjx0nmydxa2pb9trdcq', + unit_price: { + amount: '1000', + currency_code: 'GBP', + }, + }, + quantity: 1, + }, + ], + due_at: '2024-11-30T14:23:07.865592Z', + status: 'paid', + details: { + totals: { + tax: '167', + total: '1000', + subtotal: '833', + }, + line_items: [ + { + totals: { + tax: '0', + total: '1000', + subtotal: '1000', + }, + product: { + id: 'pro_01gv5dvjjx0nmydxa2pb9trdcq', + name: 'AT Test Product', + status: 'active', + image_url: null, + description: 'Exmaple', + tax_category: 'standard', + }, + quantity: 1, + tax_rate: '0', + unit_totals: { + tax: '0', + total: '1000', + subtotal: '1000', + }, + }, + ], + }, + paid_at: '2024-11-29T14:23:05.561011761Z', + checkout: null, + issued_at: '2024-11-29T14:23:07.865592Z', + address_id: 'add_01jaav7fx9ew7w6293cxjdkrp7', + created_at: '2024-11-29T14:23:05.007735Z', + updated_at: '2024-11-29T14:23:05.007735Z', + business_id: 'biz_01jaav8zw7anv2egarn5vz7xhr', + custom_data: [], + customer_id: 'ctm_01gv5gb258na82skxd7ng7ha3r', + currency_code: 'GBP', + billing_period: { + type: 'billing', + ends_at: '2024-11-30', + starts_at: '2024-11-29', + }, + invoice_number: '296-844420', + transaction_id: 'txn_01jdw4vgdq62e0b6x8dqsm4ycn', + billing_details: { + payment_terms: { + interval: 'day', + frequency: 1, + }, + enable_checkout: true, + purchase_order_number: null, + additional_information: null, + }, + }, +}; + +export const InvoicePaidMockExpectation = { + data: { + addressId: 'add_01jaav7fx9ew7w6293cxjdkrp7', + billingDetails: { + additionalInformation: null, + enableCheckout: true, + paymentTerms: { + frequency: 1, + interval: 'day', + }, + purchaseOrderNumber: null, + }, + billingPeriod: { + endsAt: '2024-11-30', + startsAt: '2024-11-29', + type: 'billing', + }, + businessId: 'biz_01jaav8zw7anv2egarn5vz7xhr', + checkout: null, + createdAt: '2024-11-29T14:23:05.007735Z', + currencyCode: 'GBP', + customData: [], + customerId: 'ctm_01gv5gb258na82skxd7ng7ha3r', + details: { + lineItems: [ + { + product: { + description: 'Exmaple', + id: 'pro_01gv5dvjjx0nmydxa2pb9trdcq', + imageUrl: null, + name: 'AT Test Product', + status: 'active', + taxCategory: 'standard', + }, + quantity: 1, + taxRate: '0', + totals: { + subtotal: '1000', + tax: '0', + total: '1000', + }, + unitTotals: { + subtotal: '1000', + tax: '0', + total: '1000', + }, + }, + ], + totals: { + subtotal: '833', + tax: '167', + total: '1000', + }, + }, + dueAt: '2024-11-30T14:23:07.865592Z', + id: 'inv_01jdw4vk9fr1n6smpbhykm6ha9', + invoiceNumber: '296-844420', + issuedAt: '2024-11-29T14:23:07.865592Z', + items: [ + { + price: { + productId: 'pro_01gv5dvjjx0nmydxa2pb9trdcq', + unitPrice: { + amount: '1000', + currencyCode: 'GBP', + }, + }, + quantity: 1, + }, + ], + paidAt: '2024-11-29T14:23:05.561011761Z', + status: 'paid', + transactionId: 'txn_01jdw4vgdq62e0b6x8dqsm4ycn', + updatedAt: '2024-11-29T14:23:05.007735Z', + }, + eventId: 'evt_01jdw4vq5a26w8mpfc59mez047', + eventType: 'invoice.paid', + notificationId: 'ntf_01h90nmerv7vrn93f97j5v72p7', + occurredAt: '2024-11-29T14:23:08.971054Z', +}; diff --git a/src/__tests__/notifications/notifications-parser.test.ts b/src/__tests__/notifications/notifications-parser.test.ts index e912d52..e65251e 100644 --- a/src/__tests__/notifications/notifications-parser.test.ts +++ b/src/__tests__/notifications/notifications-parser.test.ts @@ -114,6 +114,7 @@ import { TransactionUpdatedMock, TransactionUpdatedMockExpectation, } from '../mocks/notifications/transaction-updated.mock.js'; +import { InvoicePaidMock, InvoicePaidMockExpectation } from '../mocks/notifications/invoice-paid.mock.js'; import { IEvents, IEventsResponse } from '../../types/index.js'; import { Webhooks } from '../../notifications/index.js'; @@ -163,6 +164,8 @@ describe('Notifications Parser', () => { [TransactionPaymentFailedMock.event_type, TransactionPaymentFailedMock, TransactionPaymentFailedMockExpectation], [TransactionReadyMock.event_type, TransactionReadyMock, TransactionReadyMockExpectation], [TransactionUpdatedMock.event_type, TransactionUpdatedMock, TransactionUpdatedMockExpectation], + // Generic Event + [InvoicePaidMock.event_type, InvoicePaidMock, InvoicePaidMockExpectation], ])('validate %s ', (_eventType: string, eventMock: IEventsResponse, expectedValue: any = {}) => { expect(Webhooks.fromJson(eventMock as IEvents)).toEqual(expectedValue); }); diff --git a/src/entities/events/event-collection.ts b/src/entities/events/event-collection.ts index ba03127..f7ec178 100644 --- a/src/entities/events/event-collection.ts +++ b/src/entities/events/event-collection.ts @@ -8,8 +8,8 @@ import { type IEvents, type IEventsResponse } from '../../types/index.js'; import { Collection } from '../../internal/base/index.js'; import { type EventEntity, Webhooks } from '../../notifications/index.js'; -export class EventCollection extends Collection { - override fromJson(data: IEvents): EventEntity | null { +export class EventCollection extends Collection { + override fromJson(data: IEvents): EventEntity { return Webhooks.fromJson(data); } } diff --git a/src/entities/notifications/notification.ts b/src/entities/notifications/notification.ts index d267a28..c2bb9af 100644 --- a/src/entities/notifications/notification.ts +++ b/src/entities/notifications/notification.ts @@ -12,7 +12,7 @@ export class Notification { public readonly id: string; public readonly type: IEventName; public readonly status: NotificationStatus; - public readonly payload: EventEntity | null; + public readonly payload: EventEntity; public readonly occurredAt: string; public readonly deliveredAt: null | string; public readonly replayedAt: null | string; diff --git a/src/internal/base/index.ts b/src/internal/base/index.ts index 856ca91..84f595e 100644 --- a/src/internal/base/index.ts +++ b/src/internal/base/index.ts @@ -2,3 +2,4 @@ export * from './base-resource.js'; export * from './query-parameters.js'; export * from './path-parameters.js'; export * from './collection.js'; +export * from './transform.js'; diff --git a/src/internal/base/transform.ts b/src/internal/base/transform.ts new file mode 100644 index 0000000..011bcc2 --- /dev/null +++ b/src/internal/base/transform.ts @@ -0,0 +1,27 @@ +function toCamelCase(str: string) { + return str.toLowerCase().replace(/_([a-z])/g, (_, letter) => letter.toUpperCase()); +} + +export function convertKeysToCamelCase(obj: object): object { + // Handle null or primitive values + if (obj === null || typeof obj !== 'object') { + return obj; + } + + // Handle arrays + if (Array.isArray(obj)) { + return obj.map(convertKeysToCamelCase); + } + + // Handle objects + const converted: Record = {}; + for (const [key, value] of Object.entries(obj)) { + // Convert the key to camelCase + const camelCaseKey = toCamelCase(key); + + // Recursively convert nested objects and arrays + converted[camelCaseKey] = convertKeysToCamelCase(value); + } + + return converted; +} diff --git a/src/notifications/events/generic/generic-event.ts b/src/notifications/events/generic/generic-event.ts new file mode 100644 index 0000000..4f755b5 --- /dev/null +++ b/src/notifications/events/generic/generic-event.ts @@ -0,0 +1,12 @@ +import { Event } from '../../../entities/events/event.js'; +import { convertKeysToCamelCase } from '../../../internal/base/index.js'; +import { type IEventsResponse } from '../../../types/index.js'; + +export class GenericEvent extends Event { + public override readonly data: object; + + constructor(response: IEventsResponse) { + super(response); + this.data = convertKeysToCamelCase(response.data); + } +} diff --git a/src/notifications/events/generic/index.ts b/src/notifications/events/generic/index.ts new file mode 100644 index 0000000..db886b4 --- /dev/null +++ b/src/notifications/events/generic/index.ts @@ -0,0 +1 @@ +export * from './generic-event.js'; diff --git a/src/notifications/events/index.ts b/src/notifications/events/index.ts index 0f79974..560b6a4 100644 --- a/src/notifications/events/index.ts +++ b/src/notifications/events/index.ts @@ -9,6 +9,7 @@ export * from './adjustment/index.js'; export * from './business/index.js'; export * from './customer/index.js'; export * from './discount/index.js'; +export * from './generic/index.js'; export * from './payment-method/index.js'; export * from './payout/index.js'; export * from './price/index.js'; diff --git a/src/notifications/helpers/webhooks.ts b/src/notifications/helpers/webhooks.ts index 710626a..c1582c3 100644 --- a/src/notifications/helpers/webhooks.ts +++ b/src/notifications/helpers/webhooks.ts @@ -16,6 +16,7 @@ import { DiscountCreatedEvent, DiscountImportedEvent, DiscountUpdatedEvent, + GenericEvent, PaymentMethodDeletedEvent, PaymentMethodSavedEvent, PayoutCreatedEvent, @@ -65,7 +66,7 @@ export class Webhooks { return await new WebhooksValidator().isValidSignature(requestBody, secretKey, signature); } - static fromJson(data: IEvents): EventEntity | null { + static fromJson(data: IEvents): EventEntity { switch (data.event_type) { case EventName.AddressCreated: return new AddressCreatedEvent(data); @@ -158,7 +159,7 @@ export class Webhooks { default: // @ts-expect-error event_type did not match any handled events Logger.log(`Unknown event_type ${data.event_type}`); - return null; + return new GenericEvent(data) as EventEntity; } } }