Skip to content

Commit 0b6a18d

Browse files
committed
Merge branch 'PM-1148_handle-account-events' into HEAD
2 parents ef87b26 + 35b4e4f commit 0b6a18d

File tree

9 files changed

+298
-61
lines changed

9 files changed

+298
-61
lines changed
Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
-- CreateTable
2+
CREATE TABLE "trolley_recipient_payment_method" (
3+
"id" UUID NOT NULL DEFAULT uuid_generate_v4(),
4+
"trolley_recipient_id" INTEGER NOT NULL,
5+
"recipient_account_id" VARCHAR(80) NOT NULL,
6+
7+
CONSTRAINT "trolley_recipient_payment_method_pkey" PRIMARY KEY ("id")
8+
);
9+
10+
-- CreateIndex
11+
CREATE UNIQUE INDEX "trolley_recipient_payment_method_recipient_account_id_key" ON "trolley_recipient_payment_method"("recipient_account_id");
12+
13+
-- AddForeignKey
14+
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;

prisma/schema.prisma

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -190,6 +190,7 @@ model trolley_recipient {
190190
user_payment_method_id String @db.Uuid
191191
user_id String @unique @db.VarChar(80)
192192
trolley_id String @unique @db.VarChar(80)
193+
trolley_recipient_payment_methods trolley_recipient_payment_method[]
193194
user_payment_methods user_payment_methods @relation(fields: [user_payment_method_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_trolley_user_payment_method")
194195
}
195196

@@ -214,6 +215,13 @@ model trolley_webhook_log {
214215
updated_at DateTime? @default(now()) @db.Timestamp(6)
215216
}
216217

218+
model trolley_recipient_payment_method {
219+
id String @id @default(dbgenerated("uuid_generate_v4()")) @db.Uuid
220+
trolley_recipient_id Int
221+
recipient_account_id String @unique @db.VarChar(80)
222+
trolley_recipient trolley_recipient @relation(fields: [trolley_recipient_id], references: [id], onDelete: Cascade, onUpdate: NoAction, map: "fk_trolley_recipient_trolley_recipient_payment_method")
223+
}
224+
217225
enum action_type {
218226
INITIATE_WITHDRAWAL
219227
ADD_WITHDRAWAL_METHOD

src/api/webhooks/trolley/handlers/index.ts

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { Provider } from '@nestjs/common';
22
import { PaymentHandler } from './payment.handler';
33
import { TaxFormHandler } from './tax-form.handler';
44
import { getWebhooksEventHandlersProvider } from '../../webhooks.event-handlers.provider';
5+
import { RecipientAccountHandler } from './recipient-account.handler';
56

67
export const TrolleyWebhookHandlers: Provider[] = [
78
getWebhooksEventHandlersProvider(
@@ -10,13 +11,15 @@ export const TrolleyWebhookHandlers: Provider[] = [
1011
),
1112

1213
PaymentHandler,
14+
RecipientAccountHandler,
1315
TaxFormHandler,
1416
{
1517
provide: 'TrolleyWebhookHandlers',
16-
inject: [PaymentHandler, TaxFormHandler],
18+
inject: [PaymentHandler, RecipientAccountHandler, TaxFormHandler],
1719
useFactory: (
1820
paymentHandler: PaymentHandler,
21+
recipientAccountHandler: RecipientAccountHandler,
1922
taxFormHandler: TaxFormHandler,
20-
) => [paymentHandler, taxFormHandler],
23+
) => [paymentHandler, recipientAccountHandler, taxFormHandler],
2124
},
2225
];
Lines changed: 6 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,11 @@
11
import { Injectable } from '@nestjs/common';
2-
import { WebhookEvent } from '../../webhooks.decorators';
3-
import { TrolleyWebhookEvent } from '../trolley.types';
2+
// import { WebhookEvent } from '../../webhooks.decorators';
43

54
@Injectable()
65
export class PaymentHandler {
7-
@WebhookEvent(TrolleyWebhookEvent.paymentCreated)
8-
async handlePaymentCreated(payload: any): Promise<any> {
9-
// TODO: Build out logic for payment.created event
10-
console.log('handling', TrolleyWebhookEvent.paymentCreated);
11-
12-
}
13-
14-
@WebhookEvent(TrolleyWebhookEvent.paymentUpdated)
15-
async handlePaymentUpdated(payload: any): Promise<any> {
16-
// TODO: Build out logic for payment.updated event
17-
console.log('handling', TrolleyWebhookEvent.paymentUpdated);
18-
}
6+
// @WebhookEvent(TrolleyWebhookEvent.paymentCreated)
7+
// async handlePaymentCreated(payload: any): Promise<any> {
8+
// // TODO: Build out logic for payment.created event
9+
// console.log('handling', TrolleyWebhookEvent.paymentCreated);
10+
// }
1911
}
Lines changed: 166 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,166 @@
1+
import { Injectable } from '@nestjs/common';
2+
import { WebhookEvent } from '../../webhooks.decorators';
3+
import { PrismaService } from 'src/shared/global/prisma.service';
4+
import {
5+
RecipientAccountDeleteEventData,
6+
RecipientAccountEventData,
7+
RecipientAccountWebhookEvent,
8+
} from './recipient-account.types';
9+
import { payment_method_status } from '@prisma/client';
10+
11+
@Injectable()
12+
export class RecipientAccountHandler {
13+
constructor(private readonly prisma: PrismaService) {}
14+
15+
/**
16+
* Updates the status of the related trolley user_payment_method based on the presence of primary
17+
* Trolley payment methods associated with the recipient.
18+
*
19+
* @param recipientId - The unique identifier of the recipient in the Trolley system.
20+
*/
21+
async updateUserPaymentMethod(recipientId: string) {
22+
const recipient = await this.prisma.trolley_recipient.findFirst({
23+
where: { trolley_id: recipientId },
24+
include: {
25+
user_payment_methods: true,
26+
trolley_recipient_payment_methods: true,
27+
},
28+
});
29+
30+
if (!recipient) {
31+
console.error(
32+
`Recipient not found for recipientId '${recipientId}' while updating user payment method!`,
33+
);
34+
return;
35+
}
36+
37+
const hasPrimaryTrolleyPaymentMethod =
38+
!!recipient.trolley_recipient_payment_methods.length;
39+
40+
await this.prisma.user_payment_methods.update({
41+
where: { id: recipient.user_payment_method_id },
42+
data: {
43+
status: hasPrimaryTrolleyPaymentMethod
44+
? payment_method_status.CONNECTED
45+
: payment_method_status.INACTIVE,
46+
},
47+
});
48+
}
49+
50+
/**
51+
* Handles the creation or update of a recipient account event.
52+
*
53+
* This method processes the payload to manage the recipient's payment methods
54+
* in the database. It performs the following actions:
55+
* - Creates a new payment method if it doesn't exist and is marked as primary.
56+
* - Updates an existing payment method if it is marked as primary.
57+
* - Deletes an existing payment method if it matches the provided account ID
58+
* and is marked as inactive.
59+
* - Updates the user's payment method after processing the recipient account.
60+
*
61+
* @param payload - The data associated with the recipient account event.
62+
*/
63+
@WebhookEvent(
64+
RecipientAccountWebhookEvent.created,
65+
RecipientAccountWebhookEvent.updated,
66+
)
67+
async handleCreatedOrUpdate(
68+
payload: RecipientAccountEventData,
69+
): Promise<void> {
70+
const { recipientId, recipientAccountId } = payload;
71+
const isPrimaryPaymentMethod =
72+
payload.status === 'primary' && payload.primary === true;
73+
74+
const recipient = await this.prisma.trolley_recipient.findFirst({
75+
where: { trolley_id: recipientId },
76+
include: {
77+
user_payment_methods: true,
78+
trolley_recipient_payment_methods: true,
79+
},
80+
});
81+
82+
if (!recipient) {
83+
console.error(
84+
`Recipient not found for recipientId '${recipientId}' while updating user payment method!`,
85+
);
86+
return;
87+
}
88+
89+
const recipientPaymentMethod =
90+
recipient.trolley_recipient_payment_methods[0];
91+
92+
// create the payment method if doesn't exist & it was set to primary in trolley
93+
if (!recipientPaymentMethod && isPrimaryPaymentMethod) {
94+
await this.prisma.trolley_recipient_payment_method.create({
95+
data: {
96+
trolley_recipient_id: recipient.id,
97+
recipient_account_id: recipientAccountId,
98+
},
99+
});
100+
}
101+
102+
// no recipient, and payment method is not primary in trolley, return and do nothing
103+
if (!recipientPaymentMethod && !isPrimaryPaymentMethod) {
104+
return;
105+
}
106+
107+
// update the payment method if it exists & it was set to primary in trolley
108+
if (recipientPaymentMethod && isPrimaryPaymentMethod) {
109+
await this.prisma.trolley_recipient_payment_method.update({
110+
where: { id: recipientPaymentMethod.id },
111+
data: {
112+
recipient_account_id: recipientAccountId,
113+
},
114+
});
115+
}
116+
117+
// remove the payment method if it exists (with the same ID) and it was set as inactive in trolley
118+
if (
119+
recipientPaymentMethod &&
120+
!isPrimaryPaymentMethod &&
121+
recipientPaymentMethod.recipient_account_id === recipientAccountId
122+
) {
123+
await this.prisma.trolley_recipient_payment_method.delete({
124+
where: { id: recipientPaymentMethod.id },
125+
});
126+
}
127+
128+
await this.updateUserPaymentMethod(payload.recipientId);
129+
}
130+
131+
/**
132+
* Handles the deletion of a recipient account by removing the associated
133+
* recipient payment method and updating the user's payment method.
134+
*
135+
* @param payload - The event data containing the ID of the recipient account to be deleted.
136+
*
137+
* @remarks
138+
* - If no recipient payment method is found for the given recipient account ID,
139+
* a log message is generated, and the method exits without performing any further actions.
140+
* - Deletes the recipient payment method associated with the given recipient account ID.
141+
* - Updates the user's payment method using the trolley ID of the associated recipient.
142+
*/
143+
@WebhookEvent(RecipientAccountWebhookEvent.deleted)
144+
async handleDeleted(payload: RecipientAccountDeleteEventData): Promise<void> {
145+
const recipientPaymentMethod =
146+
await this.prisma.trolley_recipient_payment_method.findFirst({
147+
where: { recipient_account_id: payload.id },
148+
include: { trolley_recipient: true },
149+
});
150+
151+
if (!recipientPaymentMethod) {
152+
console.info(
153+
`Recipient payment method not found for recipient account id '${payload.id}' while deleting trolley payment method!`,
154+
);
155+
return;
156+
}
157+
158+
await this.prisma.trolley_recipient_payment_method.delete({
159+
where: { id: recipientPaymentMethod.id },
160+
});
161+
162+
await this.updateUserPaymentMethod(
163+
recipientPaymentMethod.trolley_recipient.trolley_id,
164+
);
165+
}
166+
}
Lines changed: 52 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,52 @@
1+
export enum RecipientAccountWebhookEvent {
2+
created = 'recipientAccount.created',
3+
updated = 'recipientAccount.updated',
4+
deleted = 'recipientAccount.deleted',
5+
}
6+
7+
export interface RecipientAccountEventDataFields {
8+
status: string;
9+
type: string;
10+
primary: boolean;
11+
currency: string;
12+
id: string;
13+
recipientId: string;
14+
recipientAccountId: string;
15+
disabledAt: string | null;
16+
recipientReferenceId: string | null;
17+
deliveryBusinessDaysEstimate: number;
18+
}
19+
20+
export interface RecipientAccountEventDataWithBankDetails
21+
extends RecipientAccountEventDataFields {
22+
country: string;
23+
iban: string;
24+
accountNum: string;
25+
bankAccountType: string | null;
26+
bankCodeMappingId: string | null;
27+
accountHolderName: string;
28+
swiftBic: string;
29+
branchId: string;
30+
bankId: string;
31+
bankName: string;
32+
bankAddress: string;
33+
bankCity: string;
34+
bankRegionCode: string;
35+
bankPostalCode: string;
36+
routeType: string;
37+
recipientFees: string;
38+
}
39+
40+
export interface RecipientAccountEventDataWithPaypalDetails
41+
extends RecipientAccountEventDataFields {
42+
emailAddress: string;
43+
}
44+
45+
export type RecipientAccountEventData =
46+
| RecipientAccountEventDataWithBankDetails
47+
| RecipientAccountEventDataWithPaypalDetails;
48+
49+
export type RecipientAccountDeleteEventData = Pick<
50+
RecipientAccountEventData,
51+
'id'
52+
>;

src/api/webhooks/trolley/handlers/tax-form.handler.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,13 @@
11
import { Injectable } from '@nestjs/common';
22
import { WebhookEvent } from '../../webhooks.decorators';
3+
import { PrismaService } from 'src/shared/global/prisma.service';
4+
import { tax_form_status, trolley_recipient } from '@prisma/client';
35
import {
46
TrolleyTaxFormStatus,
57
TaxFormStatusUpdatedEvent,
68
TaxFormStatusUpdatedEventData,
7-
TrolleyWebhookEvent,
8-
} from '../trolley.types';
9-
import { PrismaService } from 'src/shared/global/prisma.service';
10-
import { tax_form_status, trolley_recipient } from '@prisma/client';
9+
TaxFormWebhookEvent,
10+
} from './tax-form.types';
1111

1212
@Injectable()
1313
export class TaxFormHandler {
@@ -83,11 +83,11 @@ export class TaxFormHandler {
8383
* - If the recipient is found, the tax form association is created or updated
8484
* in the database.
8585
*/
86-
@WebhookEvent(TrolleyWebhookEvent.taxFormStatusUpdated)
86+
@WebhookEvent(TaxFormWebhookEvent.statusUpdated)
8787
async handleTaxFormStatusUpdated(
8888
payload: TaxFormStatusUpdatedEvent,
8989
): Promise<void> {
90-
const taxFormData = payload.taxForm.data;
90+
const taxFormData = payload.data;
9191
const recipient = await this.getDbRecipientById(taxFormData.recipientId);
9292

9393
if (!recipient) {
Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
export enum TaxFormWebhookEvent {
2+
statusUpdated = 'taxForm.status_updated',
3+
}
4+
5+
export enum TrolleyTaxFormStatus {
6+
Incomplete = 'incomplete',
7+
Submitted = 'submitted',
8+
Reviewed = 'reviewed',
9+
Voided = 'voided',
10+
}
11+
12+
export interface TaxFormStatusUpdatedEventData {
13+
recipientId: string;
14+
taxFormId: string;
15+
status: TrolleyTaxFormStatus;
16+
taxFormType: string;
17+
taxFormAddressCountry: string;
18+
mailingAddressCountry: string | null;
19+
registrationCountry: string | null;
20+
createdAt: string;
21+
signedAt: string;
22+
reviewedAt: string;
23+
reviewedBy: string;
24+
voidedAt: string | null;
25+
voidReason: string | null;
26+
voidedBy: string | null;
27+
tinStatus: string;
28+
}
29+
30+
export interface TaxFormStatusUpdatedEvent {
31+
previousFields: {
32+
status: TrolleyTaxFormStatus;
33+
};
34+
data: TaxFormStatusUpdatedEventData;
35+
}

0 commit comments

Comments
 (0)