handling if owner disconnects their stripe account

This commit is contained in:
jackiettran
2026-01-08 17:49:02 -05:00
parent 3042a9007f
commit 8585633907
6 changed files with 741 additions and 0 deletions

View File

@@ -38,6 +38,7 @@ jest.mock("../../../utils/logger", () => ({
jest.mock("../../../services/email", () => ({
payment: {
sendPayoutFailedNotification: jest.fn(),
sendAccountDisconnectedEmail: jest.fn(),
},
}));
@@ -389,4 +390,201 @@ describe("StripeWebhookService", () => {
expect(mockRental.update).toHaveBeenCalled();
});
});
describe("handleAccountDeauthorized", () => {
it("should return missing_account_id when accountId is not provided", async () => {
const result = await StripeWebhookService.handleAccountDeauthorized(null);
expect(result.processed).toBe(false);
expect(result.reason).toBe("missing_account_id");
});
it("should return missing_account_id for undefined accountId", async () => {
const result = await StripeWebhookService.handleAccountDeauthorized(undefined);
expect(result.processed).toBe(false);
expect(result.reason).toBe("missing_account_id");
});
it("should return user_not_found when account does not match any user", async () => {
User.findOne.mockResolvedValue(null);
const result = await StripeWebhookService.handleAccountDeauthorized("acct_unknown");
expect(result.processed).toBe(false);
expect(result.reason).toBe("user_not_found");
expect(User.findOne).toHaveBeenCalledWith({
where: { stripeConnectedAccountId: "acct_unknown" },
});
});
it("should clear Stripe connection fields when account is deauthorized", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: "Test",
name: "Test Owner",
stripeConnectedAccountId: "acct_123",
stripePayoutsEnabled: true,
update: jest.fn().mockResolvedValue(true),
};
User.findOne.mockResolvedValue(mockUser);
Rental.findAll.mockResolvedValue([]);
emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
success: true,
});
const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");
expect(result.processed).toBe(true);
expect(result.userId).toBe(1);
expect(mockUser.update).toHaveBeenCalledWith({
stripeConnectedAccountId: null,
stripePayoutsEnabled: false,
});
});
it("should log warning when there are pending payouts", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: "Test",
update: jest.fn().mockResolvedValue(true),
};
const mockRentals = [{ id: 100 }, { id: 101 }];
User.findOne.mockResolvedValue(mockUser);
Rental.findAll.mockResolvedValue(mockRentals);
emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
success: true,
});
const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");
expect(result.pendingPayoutsCount).toBe(2);
expect(Rental.findAll).toHaveBeenCalledWith({
where: {
ownerId: 1,
payoutStatus: "pending",
},
});
});
it("should send notification email to owner", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: "Test",
update: jest.fn().mockResolvedValue(true),
};
User.findOne.mockResolvedValue(mockUser);
Rental.findAll.mockResolvedValue([]);
emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
success: true,
});
const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");
expect(result.notificationSent).toBe(true);
expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
"owner@test.com",
{
ownerName: "Test",
hasPendingPayouts: false,
pendingPayoutCount: 0,
}
);
});
it("should send notification with pending payout info when there are pending payouts", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: "Owner",
update: jest.fn().mockResolvedValue(true),
};
const mockRentals = [{ id: 100 }, { id: 101 }, { id: 102 }];
User.findOne.mockResolvedValue(mockUser);
Rental.findAll.mockResolvedValue(mockRentals);
emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
success: true,
});
await StripeWebhookService.handleAccountDeauthorized("acct_123");
expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
"owner@test.com",
{
ownerName: "Owner",
hasPendingPayouts: true,
pendingPayoutCount: 3,
}
);
});
it("should use name fallback when firstName is not available", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: null,
name: "Full Name",
update: jest.fn().mockResolvedValue(true),
};
User.findOne.mockResolvedValue(mockUser);
Rental.findAll.mockResolvedValue([]);
emailServices.payment.sendAccountDisconnectedEmail.mockResolvedValue({
success: true,
});
await StripeWebhookService.handleAccountDeauthorized("acct_123");
expect(emailServices.payment.sendAccountDisconnectedEmail).toHaveBeenCalledWith(
"owner@test.com",
expect.objectContaining({
ownerName: "Full Name",
})
);
});
it("should handle email sending failure gracefully", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: "Test",
update: jest.fn().mockResolvedValue(true),
};
User.findOne.mockResolvedValue(mockUser);
Rental.findAll.mockResolvedValue([]);
emailServices.payment.sendAccountDisconnectedEmail.mockRejectedValue(
new Error("Email service down")
);
const result = await StripeWebhookService.handleAccountDeauthorized("acct_123");
// Should still mark as processed even if notification fails
expect(result.processed).toBe(true);
expect(result.notificationSent).toBe(false);
expect(mockUser.update).toHaveBeenCalled();
});
it("should throw error when database update fails", async () => {
const mockUser = {
id: 1,
email: "owner@test.com",
firstName: "Test",
update: jest.fn().mockRejectedValue(new Error("DB error")),
};
User.findOne.mockResolvedValue(mockUser);
await expect(
StripeWebhookService.handleAccountDeauthorized("acct_123")
).rejects.toThrow("DB error");
});
});
});