From 660b7e3c02a26015795b2f9e9ad37c1205281d6a Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 29 Apr 2025 10:58:17 +0300 Subject: [PATCH 1/3] PM-1148 - handle account events --- .../migration.sql | 14 ++ prisma/schema.prisma | 8 + src/api/webhooks/trolley/handlers/index.ts | 7 +- .../trolley/handlers/payment.handler.ts | 20 +-- .../handlers/recipient-account.handler.ts | 166 ++++++++++++++++++ .../handlers/recipient-account.types.ts | 39 ++++ .../trolley/handlers/tax-form.handler.ts | 12 +- .../trolley/handlers/tax-form.types.ts | 35 ++++ src/api/webhooks/trolley/trolley.types.ts | 45 +---- 9 files changed, 285 insertions(+), 61 deletions(-) create mode 100644 prisma/migrations/20250428162845_add_trolley_recipient_payment_method/migration.sql create mode 100644 src/api/webhooks/trolley/handlers/recipient-account.handler.ts create mode 100644 src/api/webhooks/trolley/handlers/recipient-account.types.ts create mode 100644 src/api/webhooks/trolley/handlers/tax-form.types.ts diff --git a/prisma/migrations/20250428162845_add_trolley_recipient_payment_method/migration.sql b/prisma/migrations/20250428162845_add_trolley_recipient_payment_method/migration.sql new file mode 100644 index 0000000..9148ce1 --- /dev/null +++ b/prisma/migrations/20250428162845_add_trolley_recipient_payment_method/migration.sql @@ -0,0 +1,14 @@ +-- CreateTable +CREATE TABLE "trolley_recipient_payment_method" ( + "id" UUID NOT NULL DEFAULT uuid_generate_v4(), + "trolley_recipient_id" INTEGER NOT NULL, + "recipient_account_id" VARCHAR(80) NOT NULL, + + CONSTRAINT "trolley_recipient_payment_method_pkey" PRIMARY KEY ("id") +); + +-- CreateIndex +CREATE UNIQUE INDEX "trolley_recipient_payment_method_recipient_account_id_key" ON "trolley_recipient_payment_method"("recipient_account_id"); + +-- AddForeignKey +ALTER TABLE "trolley_recipient_payment_method" ADD CONSTRAINT "fk_trolley_recipient_trolley_recipient_payment_method" FOREIGN KEY ("trolley_recipient_id") REFERENCES "trolley_recipient"("id") ON DELETE CASCADE ON UPDATE NO ACTION; diff --git a/prisma/schema.prisma b/prisma/schema.prisma index 265930f..5077935 100644 --- a/prisma/schema.prisma +++ b/prisma/schema.prisma @@ -190,6 +190,7 @@ model trolley_recipient { user_payment_method_id String @db.Uuid user_id String @unique @db.VarChar(80) trolley_id String @unique @db.VarChar(80) + trolley_recipient_payment_methods trolley_recipient_payment_method[] user_payment_methods user_payment_methods @relation(fields: [user_payment_method_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_trolley_user_payment_method") } @@ -214,6 +215,13 @@ model trolley_webhook_log { updated_at DateTime? @default(now()) @db.Timestamp(6) } +model trolley_recipient_payment_method { + id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid + trolley_recipient_id Int + recipient_account_id String @unique @db.VarChar(80) + trolley_recipient trolley_recipient @relation(fields: [trolley_recipient_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_trolley_recipient_trolley_recipient_payment_method") +} + enum action_type { INITIATE_WITHDRAWAL ADD_WITHDRAWAL_METHOD diff --git a/src/api/webhooks/trolley/handlers/index.ts b/src/api/webhooks/trolley/handlers/index.ts index 405e980..0916b44 100644 --- a/src/api/webhooks/trolley/handlers/index.ts +++ b/src/api/webhooks/trolley/handlers/index.ts @@ -2,6 +2,7 @@ import { Provider } from '@nestjs/common'; import { PaymentHandler } from './payment.handler'; import { TaxFormHandler } from './tax-form.handler'; import { getWebhooksEventHandlersProvider } from '../../webhooks.event-handlers.provider'; +import { RecipientAccountHandler } from './recipient-account.handler'; export const TrolleyWebhookHandlers: Provider[] = [ getWebhooksEventHandlersProvider( @@ -10,13 +11,15 @@ export const TrolleyWebhookHandlers: Provider[] = [ ), PaymentHandler, + RecipientAccountHandler, TaxFormHandler, { provide: 'TrolleyWebhookHandlers', - inject: [PaymentHandler, TaxFormHandler], + inject: [PaymentHandler, RecipientAccountHandler, TaxFormHandler], useFactory: ( paymentHandler: PaymentHandler, + recipientAccountHandler: RecipientAccountHandler, taxFormHandler: TaxFormHandler, - ) => [paymentHandler, taxFormHandler], + ) => [paymentHandler, recipientAccountHandler, taxFormHandler], }, ]; diff --git a/src/api/webhooks/trolley/handlers/payment.handler.ts b/src/api/webhooks/trolley/handlers/payment.handler.ts index a66e691..cdc4734 100644 --- a/src/api/webhooks/trolley/handlers/payment.handler.ts +++ b/src/api/webhooks/trolley/handlers/payment.handler.ts @@ -1,19 +1,11 @@ import { Injectable } from '@nestjs/common'; -import { WebhookEvent } from '../../webhooks.decorators'; -import { TrolleyWebhookEvent } from '../trolley.types'; +// import { WebhookEvent } from '../../webhooks.decorators'; @Injectable() export class PaymentHandler { - @WebhookEvent(TrolleyWebhookEvent.paymentCreated) - async handlePaymentCreated(payload: any): Promise { - // TODO: Build out logic for payment.created event - console.log('handling', TrolleyWebhookEvent.paymentCreated); - - } - - @WebhookEvent(TrolleyWebhookEvent.paymentUpdated) - async handlePaymentUpdated(payload: any): Promise { - // TODO: Build out logic for payment.updated event - console.log('handling', TrolleyWebhookEvent.paymentUpdated); - } + // @WebhookEvent(TrolleyWebhookEvent.paymentCreated) + // async handlePaymentCreated(payload: any): Promise { + // // TODO: Build out logic for payment.created event + // console.log('handling', TrolleyWebhookEvent.paymentCreated); + // } } diff --git a/src/api/webhooks/trolley/handlers/recipient-account.handler.ts b/src/api/webhooks/trolley/handlers/recipient-account.handler.ts new file mode 100644 index 0000000..4546739 --- /dev/null +++ b/src/api/webhooks/trolley/handlers/recipient-account.handler.ts @@ -0,0 +1,166 @@ +import { Injectable } from '@nestjs/common'; +import { WebhookEvent } from '../../webhooks.decorators'; +import { PrismaService } from 'src/shared/global/prisma.service'; +import { + RecipientAccountDeleteEventData, + RecipientAccountEventData, + RecipientAccountWebhookEvent, +} from './recipient-account.types'; +import { payment_method_status } from '@prisma/client'; + +@Injectable() +export class RecipientAccountHandler { + constructor(private readonly prisma: PrismaService) {} + + /** + * Updates the status of the related trolley user_payment_method based on the presence of primary + * Trolley payment methods associated with the recipient. + * + * @param recipientId - The unique identifier of the recipient in the Trolley system. + */ + async updateUserPaymentMethod(recipientId: string) { + const recipient = await this.prisma.trolley_recipient.findFirst({ + where: { trolley_id: recipientId }, + include: { + user_payment_methods: true, + trolley_recipient_payment_methods: true, + }, + }); + + if (!recipient) { + console.error( + `Recipient not found for recipientId '${recipientId}' while updating user payment method!`, + ); + return; + } + + const hasPrimaryTrolleyPaymentMethod = + !!recipient.trolley_recipient_payment_methods.length; + + await this.prisma.user_payment_methods.update({ + where: { id: recipient.user_payment_method_id }, + data: { + status: hasPrimaryTrolleyPaymentMethod + ? payment_method_status.CONNECTED + : payment_method_status.INACTIVE, + }, + }); + } + + /** + * Handles the creation or update of a recipient account event. + * + * This method processes the payload to manage the recipient's payment methods + * in the database. It performs the following actions: + * - Creates a new payment method if it doesn't exist and is marked as primary. + * - Updates an existing payment method if it is marked as primary. + * - Deletes an existing payment method if it matches the provided account ID + * and is marked as inactive. + * - Updates the user's payment method after processing the recipient account. + * + * @param payload - The data associated with the recipient account event. + */ + @WebhookEvent( + RecipientAccountWebhookEvent.created, + RecipientAccountWebhookEvent.updated, + ) + async handleCreatedOrUpdate( + payload: RecipientAccountEventData, + ): Promise { + const { recipientId, recipientAccountId } = payload; + const isPrimaryPaymentMethod = + payload.status === 'primary' && payload.primary === true; + + const recipient = await this.prisma.trolley_recipient.findFirst({ + where: { trolley_id: recipientId }, + include: { + user_payment_methods: true, + trolley_recipient_payment_methods: true, + }, + }); + + if (!recipient) { + console.error( + `Recipient not found for recipientId '${recipientId}' while updating user payment method!`, + ); + return; + } + + const recipientPaymentMethod = + recipient.trolley_recipient_payment_methods[0]; + + // create the payment method if doesn't exist & it was set to primary in trolley + if (!recipientPaymentMethod && isPrimaryPaymentMethod) { + await this.prisma.trolley_recipient_payment_method.create({ + data: { + trolley_recipient_id: recipient.id, + recipient_account_id: recipientAccountId, + }, + }); + return; + } + + // no recipient, and payment method is not primary in trolley, return and do nothing + if (!recipientPaymentMethod && !isPrimaryPaymentMethod) { + return; + } + + // update the payment method if it exists & it was set to primary in trolley + if (isPrimaryPaymentMethod) { + await this.prisma.trolley_recipient_payment_method.update({ + where: { id: recipientPaymentMethod.id }, + data: { + recipient_account_id: recipientAccountId, + }, + }); + } + + // remove the payment method if it exists (with the same ID) and it was set as inactive in trolley + if ( + !isPrimaryPaymentMethod && + recipientPaymentMethod.recipient_account_id === recipientAccountId + ) { + await this.prisma.trolley_recipient_payment_method.delete({ + where: { id: recipientPaymentMethod.id }, + }); + } + + await this.updateUserPaymentMethod(payload.recipientId); + } + + /** + * Handles the deletion of a recipient account by removing the associated + * recipient payment method and updating the user's payment method. + * + * @param payload - The event data containing the ID of the recipient account to be deleted. + * + * @remarks + * - If no recipient payment method is found for the given recipient account ID, + * a log message is generated, and the method exits without performing any further actions. + * - Deletes the recipient payment method associated with the given recipient account ID. + * - Updates the user's payment method using the trolley ID of the associated recipient. + */ + @WebhookEvent(RecipientAccountWebhookEvent.deleted) + async handleDeleted(payload: RecipientAccountDeleteEventData): Promise { + const recipientPaymentMethod = + await this.prisma.trolley_recipient_payment_method.findFirst({ + where: { recipient_account_id: payload.id }, + include: { trolley_recipient: true }, + }); + + if (!recipientPaymentMethod) { + console.info( + `Recipient payment method not found for recipient account id '${payload.id}' while deleting trolley payment method!`, + ); + return; + } + + await this.prisma.trolley_recipient_payment_method.delete({ + where: { id: recipientPaymentMethod.id }, + }); + + await this.updateUserPaymentMethod( + recipientPaymentMethod.trolley_recipient.trolley_id, + ); + } +} diff --git a/src/api/webhooks/trolley/handlers/recipient-account.types.ts b/src/api/webhooks/trolley/handlers/recipient-account.types.ts new file mode 100644 index 0000000..5056c2b --- /dev/null +++ b/src/api/webhooks/trolley/handlers/recipient-account.types.ts @@ -0,0 +1,39 @@ +export enum RecipientAccountWebhookEvent { + created = 'recipientAccount.created', + updated = 'recipientAccount.updated', + deleted = 'recipientAccount.deleted', +} + +export interface RecipientAccountEventData { + status: string; + type: string; + primary: boolean; + currency: string; + id: string; + recipientId: string; + recipientAccountId: string; + disabledAt: string | null; + recipientReferenceId: string | null; + deliveryBusinessDaysEstimate: number; + country: string; + iban: string; + accountNum: string; + bankAccountType: string | null; + bankCodeMappingId: string | null; + accountHolderName: string; + swiftBic: string; + branchId: string; + bankId: string; + bankName: string; + bankAddress: string; + bankCity: string; + bankRegionCode: string; + bankPostalCode: string; + routeType: string; + recipientFees: string; +} + +export type RecipientAccountDeleteEventData = Pick< + RecipientAccountEventData, + 'id' +>; diff --git a/src/api/webhooks/trolley/handlers/tax-form.handler.ts b/src/api/webhooks/trolley/handlers/tax-form.handler.ts index e5d610a..9cb41cd 100644 --- a/src/api/webhooks/trolley/handlers/tax-form.handler.ts +++ b/src/api/webhooks/trolley/handlers/tax-form.handler.ts @@ -1,13 +1,13 @@ import { Injectable } from '@nestjs/common'; import { WebhookEvent } from '../../webhooks.decorators'; +import { PrismaService } from 'src/shared/global/prisma.service'; +import { tax_form_status, trolley_recipient } from '@prisma/client'; import { TaxFormStatus, TaxFormStatusUpdatedEvent, TaxFormStatusUpdatedEventData, - TrolleyWebhookEvent, -} from '../trolley.types'; -import { PrismaService } from 'src/shared/global/prisma.service'; -import { tax_form_status, trolley_recipient } from '@prisma/client'; + TaxFormWebhookEvent, +} from './tax-form.types'; @Injectable() export class TaxFormHandler { @@ -80,11 +80,11 @@ export class TaxFormHandler { * - If the recipient is found, the tax form association is created or updated * in the database. */ - @WebhookEvent(TrolleyWebhookEvent.taxFormStatusUpdated) + @WebhookEvent(TaxFormWebhookEvent.statusUpdated) async handleTaxFormStatusUpdated( payload: TaxFormStatusUpdatedEvent, ): Promise { - const taxFormData = payload.taxForm.data; + const taxFormData = payload.data; const recipient = await this.getDbRecipientById(taxFormData.recipientId); if (!recipient) { diff --git a/src/api/webhooks/trolley/handlers/tax-form.types.ts b/src/api/webhooks/trolley/handlers/tax-form.types.ts new file mode 100644 index 0000000..c859f09 --- /dev/null +++ b/src/api/webhooks/trolley/handlers/tax-form.types.ts @@ -0,0 +1,35 @@ +export enum TaxFormWebhookEvent { + statusUpdated = 'taxForm.status_updated', +} + +export enum TaxFormStatus { + Incomplete = 'incomplete', + Submitted = 'submitted', + Reviewed = 'reviewed', + Voided = 'voided', +} + +export interface TaxFormStatusUpdatedEventData { + recipientId: string; + taxFormId: string; + status: TaxFormStatus; + taxFormType: string; + taxFormAddressCountry: string; + mailingAddressCountry: string | null; + registrationCountry: string | null; + createdAt: string; + signedAt: string; + reviewedAt: string; + reviewedBy: string; + voidedAt: string | null; + voidReason: string | null; + voidedBy: string | null; + tinStatus: string; +} + +export interface TaxFormStatusUpdatedEvent { + previousFields: { + status: TaxFormStatus; + }; + data: TaxFormStatusUpdatedEventData; +} diff --git a/src/api/webhooks/trolley/trolley.types.ts b/src/api/webhooks/trolley/trolley.types.ts index e451187..ac5c20b 100644 --- a/src/api/webhooks/trolley/trolley.types.ts +++ b/src/api/webhooks/trolley/trolley.types.ts @@ -1,41 +1,8 @@ -export enum TrolleyWebhookEvent { - paymentCreated = 'payment.created', - paymentUpdated = 'payment.updated', - taxFormStatusUpdated = 'taxForm.status_updated', -} +import { RecipientAccountWebhookEvent } from './handlers/recipient-account.types'; +import { TaxFormWebhookEvent } from './handlers/tax-form.types'; -export type TrolleyEventHandler = (eventPayload: any) => Promise; - -export enum TaxFormStatus { - Incomplete = 'incomplete', - Submitted = 'submitted', - Reviewed = 'reviewed', - Voided = 'voided', -} +export type TrolleyWebhookEvent = + | RecipientAccountWebhookEvent + | TaxFormWebhookEvent; -export interface TaxFormStatusUpdatedEventData { - recipientId: string; - taxFormId: string; - status: TaxFormStatus; - taxFormType: string; - taxFormAddressCountry: string; - mailingAddressCountry: string | null; - registrationCountry: string | null; - createdAt: string; - signedAt: string; - reviewedAt: string; - reviewedBy: string; - voidedAt: string | null; - voidReason: string | null; - voidedBy: string | null; - tinStatus: string; -} - -export interface TaxFormStatusUpdatedEvent { - taxForm: { - previousFields: { - status: TaxFormStatus; - }; - data: TaxFormStatusUpdatedEventData; - }; -} +export type TrolleyEventHandler = (eventPayload: any) => Promise; From 45a9ea8bce620995f8b093c7946bb34b00c03108 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 29 Apr 2025 11:10:38 +0300 Subject: [PATCH 2/3] Fix logic --- .../webhooks/trolley/handlers/recipient-account.handler.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/api/webhooks/trolley/handlers/recipient-account.handler.ts b/src/api/webhooks/trolley/handlers/recipient-account.handler.ts index 4546739..0f15259 100644 --- a/src/api/webhooks/trolley/handlers/recipient-account.handler.ts +++ b/src/api/webhooks/trolley/handlers/recipient-account.handler.ts @@ -97,7 +97,6 @@ export class RecipientAccountHandler { recipient_account_id: recipientAccountId, }, }); - return; } // no recipient, and payment method is not primary in trolley, return and do nothing @@ -106,7 +105,7 @@ export class RecipientAccountHandler { } // update the payment method if it exists & it was set to primary in trolley - if (isPrimaryPaymentMethod) { + if (recipientPaymentMethod && isPrimaryPaymentMethod) { await this.prisma.trolley_recipient_payment_method.update({ where: { id: recipientPaymentMethod.id }, data: { @@ -117,6 +116,7 @@ export class RecipientAccountHandler { // remove the payment method if it exists (with the same ID) and it was set as inactive in trolley if ( + recipientPaymentMethod && !isPrimaryPaymentMethod && recipientPaymentMethod.recipient_account_id === recipientAccountId ) { From 35b4e4f2d53e2ff731e42eaa258de0111a1e3c65 Mon Sep 17 00:00:00 2001 From: Vasilica Olariu Date: Tue, 29 Apr 2025 13:25:46 +0300 Subject: [PATCH 3/3] PM-1148 - update event interface for recipient account --- .../trolley/handlers/recipient-account.types.ts | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/api/webhooks/trolley/handlers/recipient-account.types.ts b/src/api/webhooks/trolley/handlers/recipient-account.types.ts index 5056c2b..ed82b48 100644 --- a/src/api/webhooks/trolley/handlers/recipient-account.types.ts +++ b/src/api/webhooks/trolley/handlers/recipient-account.types.ts @@ -4,7 +4,7 @@ export enum RecipientAccountWebhookEvent { deleted = 'recipientAccount.deleted', } -export interface RecipientAccountEventData { +export interface RecipientAccountEventDataFields { status: string; type: string; primary: boolean; @@ -15,6 +15,10 @@ export interface RecipientAccountEventData { disabledAt: string | null; recipientReferenceId: string | null; deliveryBusinessDaysEstimate: number; +} + +export interface RecipientAccountEventDataWithBankDetails + extends RecipientAccountEventDataFields { country: string; iban: string; accountNum: string; @@ -33,6 +37,15 @@ export interface RecipientAccountEventData { recipientFees: string; } +export interface RecipientAccountEventDataWithPaypalDetails + extends RecipientAccountEventDataFields { + emailAddress: string; +} + +export type RecipientAccountEventData = + | RecipientAccountEventDataWithBankDetails + | RecipientAccountEventDataWithPaypalDetails; + export type RecipientAccountDeleteEventData = Pick< RecipientAccountEventData, 'id'