Skip to content

PM-1148 - handle account events #36

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Apr 30, 2025
Merged
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
Original file line number Diff line number Diff line change
@@ -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;
8 changes: 8 additions & 0 deletions prisma/schema.prisma
Original file line number Diff line number Diff line change
Expand Up @@ -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")
}

Expand All @@ -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
Expand Down
7 changes: 5 additions & 2 deletions src/api/webhooks/trolley/handlers/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -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],
},
];
20 changes: 6 additions & 14 deletions src/api/webhooks/trolley/handlers/payment.handler.ts
Original file line number Diff line number Diff line change
@@ -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<any> {
// TODO: Build out logic for payment.created event
console.log('handling', TrolleyWebhookEvent.paymentCreated);

}

@WebhookEvent(TrolleyWebhookEvent.paymentUpdated)
async handlePaymentUpdated(payload: any): Promise<any> {
// TODO: Build out logic for payment.updated event
console.log('handling', TrolleyWebhookEvent.paymentUpdated);
}
// @WebhookEvent(TrolleyWebhookEvent.paymentCreated)
// async handlePaymentCreated(payload: any): Promise<any> {
// // TODO: Build out logic for payment.created event
// console.log('handling', TrolleyWebhookEvent.paymentCreated);
// }
}
166 changes: 166 additions & 0 deletions src/api/webhooks/trolley/handlers/recipient-account.handler.ts
Original file line number Diff line number Diff line change
@@ -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<void> {
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,
},
});
}

// 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 (recipientPaymentMethod && 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 (
recipientPaymentMethod &&
!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<void> {
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,
);
}
}
52 changes: 52 additions & 0 deletions src/api/webhooks/trolley/handlers/recipient-account.types.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,52 @@
export enum RecipientAccountWebhookEvent {
created = 'recipientAccount.created',
updated = 'recipientAccount.updated',
deleted = 'recipientAccount.deleted',
}

export interface RecipientAccountEventDataFields {
status: string;
type: string;
primary: boolean;
currency: string;
id: string;
recipientId: string;
recipientAccountId: string;
disabledAt: string | null;
recipientReferenceId: string | null;
deliveryBusinessDaysEstimate: number;
}

export interface RecipientAccountEventDataWithBankDetails
extends RecipientAccountEventDataFields {
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 interface RecipientAccountEventDataWithPaypalDetails
extends RecipientAccountEventDataFields {
emailAddress: string;
}

export type RecipientAccountEventData =
| RecipientAccountEventDataWithBankDetails
| RecipientAccountEventDataWithPaypalDetails;

export type RecipientAccountDeleteEventData = Pick<
RecipientAccountEventData,
'id'
>;
12 changes: 6 additions & 6 deletions src/api/webhooks/trolley/handlers/tax-form.handler.ts
Original file line number Diff line number Diff line change
@@ -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 {
Expand Down Expand Up @@ -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<void> {
const taxFormData = payload.taxForm.data;
const taxFormData = payload.data;
const recipient = await this.getDbRecipientById(taxFormData.recipientId);

if (!recipient) {
Expand Down
35 changes: 35 additions & 0 deletions src/api/webhooks/trolley/handlers/tax-form.types.ts
Original file line number Diff line number Diff line change
@@ -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;
}
Loading