backend unit test coverage to 80%
This commit is contained in:
@@ -2,6 +2,7 @@
|
||||
jest.mock('../../../models', () => ({
|
||||
Rental: {
|
||||
findAll: jest.fn(),
|
||||
findByPk: jest.fn(),
|
||||
update: jest.fn()
|
||||
},
|
||||
User: jest.fn(),
|
||||
@@ -12,6 +13,14 @@ jest.mock('../../../services/stripeService', () => ({
|
||||
createTransfer: jest.fn()
|
||||
}));
|
||||
|
||||
// Mock email services
|
||||
const mockSendPayoutReceivedEmail = jest.fn();
|
||||
jest.mock('../../../services/email', () => ({
|
||||
rentalFlow: {
|
||||
sendPayoutReceivedEmail: mockSendPayoutReceivedEmail
|
||||
}
|
||||
}));
|
||||
|
||||
jest.mock('sequelize', () => ({
|
||||
Op: {
|
||||
not: 'not'
|
||||
@@ -37,6 +46,7 @@ const StripeService = require('../../../services/stripeService');
|
||||
|
||||
// Get references to mocks after importing
|
||||
const mockRentalFindAll = Rental.findAll;
|
||||
const mockRentalFindByPk = Rental.findByPk;
|
||||
const mockRentalUpdate = Rental.update;
|
||||
const mockUserModel = User;
|
||||
const mockItemModel = Item;
|
||||
@@ -755,4 +765,284 @@ describe('PayoutService', () => {
|
||||
expect(result.amount).toBe(999999999);
|
||||
});
|
||||
});
|
||||
|
||||
describe('triggerPayoutOnCompletion', () => {
|
||||
beforeEach(() => {
|
||||
mockRentalFindByPk.mockReset();
|
||||
mockSendPayoutReceivedEmail.mockReset();
|
||||
});
|
||||
|
||||
it('should return rental_not_found when rental does not exist', async () => {
|
||||
mockRentalFindByPk.mockResolvedValue(null);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('nonexistent-rental-id');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'rental_not_found'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payment_not_paid when paymentStatus is not paid', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
paymentStatus: 'pending',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'payment_not_paid'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payout_not_pending when payoutStatus is not pending', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'completed',
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'payout_not_pending'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return no_stripe_account when owner has no stripeConnectedAccountId', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 1,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
stripeConnectedAccountId: null,
|
||||
stripePayoutsEnabled: false
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'no_stripe_account'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payouts_not_enabled when owner stripePayoutsEnabled is false', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 1,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
owner: {
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: false
|
||||
}
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: false,
|
||||
success: false,
|
||||
reason: 'payouts_not_enabled'
|
||||
});
|
||||
});
|
||||
|
||||
it('should successfully process payout when all conditions are met', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 2,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
totalAmount: 10000,
|
||||
platformFee: 500,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
email: 'owner@example.com',
|
||||
firstName: 'John',
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_success_123',
|
||||
amount: 9500
|
||||
});
|
||||
mockSendPayoutReceivedEmail.mockResolvedValue(true);
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: true,
|
||||
success: true,
|
||||
transferId: 'tr_success_123',
|
||||
amount: 9500
|
||||
});
|
||||
expect(mockCreateTransfer).toHaveBeenCalled();
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
payoutStatus: 'completed',
|
||||
payoutProcessedAt: expect.any(Date),
|
||||
stripeTransferId: 'tr_success_123'
|
||||
});
|
||||
});
|
||||
|
||||
it('should return payout_failed on processRentalPayout error', async () => {
|
||||
const mockRental = {
|
||||
id: 'rental-123',
|
||||
ownerId: 2,
|
||||
paymentStatus: 'paid',
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
totalAmount: 10000,
|
||||
platformFee: 500,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
email: 'owner@example.com',
|
||||
firstName: 'John',
|
||||
stripeConnectedAccountId: 'acct_123',
|
||||
stripePayoutsEnabled: true
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
mockRentalFindByPk.mockResolvedValue(mockRental);
|
||||
mockCreateTransfer.mockRejectedValue(new Error('Stripe transfer failed'));
|
||||
|
||||
const result = await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(result).toEqual({
|
||||
attempted: true,
|
||||
success: false,
|
||||
reason: 'payout_failed',
|
||||
error: 'Stripe transfer failed'
|
||||
});
|
||||
});
|
||||
|
||||
it('should include Item model in findByPk query', async () => {
|
||||
mockRentalFindByPk.mockResolvedValue(null);
|
||||
|
||||
await PayoutService.triggerPayoutOnCompletion('rental-123');
|
||||
|
||||
expect(mockRentalFindByPk).toHaveBeenCalledWith('rental-123', {
|
||||
include: [
|
||||
{
|
||||
model: mockUserModel,
|
||||
as: 'owner',
|
||||
attributes: ['id', 'email', 'firstName', 'lastName', 'stripeConnectedAccountId', 'stripePayoutsEnabled']
|
||||
},
|
||||
{ model: mockItemModel, as: 'item' }
|
||||
]
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('processRentalPayout - email notifications', () => {
|
||||
let mockRental;
|
||||
|
||||
beforeEach(() => {
|
||||
mockSendPayoutReceivedEmail.mockReset();
|
||||
mockRental = {
|
||||
id: 1,
|
||||
ownerId: 2,
|
||||
payoutStatus: 'pending',
|
||||
payoutAmount: 9500,
|
||||
totalAmount: 10000,
|
||||
platformFee: 500,
|
||||
startDateTime: new Date('2023-01-01T10:00:00Z'),
|
||||
endDateTime: new Date('2023-01-02T10:00:00Z'),
|
||||
owner: {
|
||||
id: 2,
|
||||
email: 'owner@example.com',
|
||||
firstName: 'John',
|
||||
stripeConnectedAccountId: 'acct_123'
|
||||
},
|
||||
update: jest.fn().mockResolvedValue(true)
|
||||
};
|
||||
mockCreateTransfer.mockResolvedValue({
|
||||
id: 'tr_123456789',
|
||||
amount: 9500
|
||||
});
|
||||
});
|
||||
|
||||
it('should send payout notification email on successful payout', async () => {
|
||||
mockSendPayoutReceivedEmail.mockResolvedValue(true);
|
||||
|
||||
await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
expect(mockSendPayoutReceivedEmail).toHaveBeenCalledWith(
|
||||
mockRental.owner,
|
||||
mockRental
|
||||
);
|
||||
expect(mockLoggerInfo).toHaveBeenCalledWith(
|
||||
'Payout notification email sent to owner',
|
||||
expect.objectContaining({
|
||||
rentalId: 1,
|
||||
ownerId: 2
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should continue successfully even if email sending fails', async () => {
|
||||
mockSendPayoutReceivedEmail.mockRejectedValue(new Error('Email service unavailable'));
|
||||
|
||||
const result = await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
// Payout should still succeed
|
||||
expect(result).toEqual({
|
||||
success: true,
|
||||
transferId: 'tr_123456789',
|
||||
amount: 9500
|
||||
});
|
||||
|
||||
// Error should be logged
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Failed to send payout notification email',
|
||||
expect.objectContaining({
|
||||
error: 'Email service unavailable',
|
||||
rentalId: 1,
|
||||
ownerId: 2
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should still update rental status even if email fails', async () => {
|
||||
mockSendPayoutReceivedEmail.mockRejectedValue(new Error('Email error'));
|
||||
|
||||
await PayoutService.processRentalPayout(mockRental);
|
||||
|
||||
expect(mockRental.update).toHaveBeenCalledWith({
|
||||
payoutStatus: 'completed',
|
||||
payoutProcessedAt: expect.any(Date),
|
||||
stripeTransferId: 'tr_123456789'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -9,6 +9,8 @@ const mockStripeRefundsRetrieve = jest.fn();
|
||||
const mockStripePaymentIntentsCreate = jest.fn();
|
||||
const mockStripeCustomersCreate = jest.fn();
|
||||
const mockStripeCheckoutSessionsCreate = jest.fn();
|
||||
const mockStripeAccountSessionsCreate = jest.fn();
|
||||
const mockStripePaymentMethodsRetrieve = jest.fn();
|
||||
|
||||
jest.mock('stripe', () => {
|
||||
return jest.fn(() => ({
|
||||
@@ -25,6 +27,9 @@ jest.mock('stripe', () => {
|
||||
accountLinks: {
|
||||
create: mockStripeAccountLinksCreate
|
||||
},
|
||||
accountSessions: {
|
||||
create: mockStripeAccountSessionsCreate
|
||||
},
|
||||
transfers: {
|
||||
create: mockStripeTransfersCreate
|
||||
},
|
||||
@@ -37,15 +42,20 @@ jest.mock('stripe', () => {
|
||||
},
|
||||
customers: {
|
||||
create: mockStripeCustomersCreate
|
||||
},
|
||||
paymentMethods: {
|
||||
retrieve: mockStripePaymentMethodsRetrieve
|
||||
}
|
||||
}));
|
||||
});
|
||||
|
||||
const mockLoggerError = jest.fn();
|
||||
const mockLoggerWarn = jest.fn();
|
||||
const mockLoggerInfo = jest.fn();
|
||||
jest.mock('../../../utils/logger', () => ({
|
||||
error: mockLoggerError,
|
||||
info: jest.fn(),
|
||||
warn: jest.fn(),
|
||||
info: mockLoggerInfo,
|
||||
warn: mockLoggerWarn,
|
||||
withRequestId: jest.fn(() => ({
|
||||
error: jest.fn(),
|
||||
info: jest.fn(),
|
||||
@@ -53,6 +63,23 @@ jest.mock('../../../utils/logger', () => ({
|
||||
})),
|
||||
}));
|
||||
|
||||
// Mock User model
|
||||
const mockUserFindOne = jest.fn();
|
||||
const mockUserUpdate = jest.fn();
|
||||
jest.mock('../../../models', () => ({
|
||||
User: {
|
||||
findOne: mockUserFindOne
|
||||
}
|
||||
}));
|
||||
|
||||
// Mock email services
|
||||
const mockSendAccountDisconnectedEmail = jest.fn();
|
||||
jest.mock('../../../services/email', () => ({
|
||||
payment: {
|
||||
sendAccountDisconnectedEmail: mockSendAccountDisconnectedEmail
|
||||
}
|
||||
}));
|
||||
|
||||
const StripeService = require('../../../services/stripeService');
|
||||
|
||||
describe('StripeService', () => {
|
||||
@@ -1158,4 +1185,500 @@ describe('StripeService', () => {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('createAccountSession', () => {
|
||||
it('should create account session successfully', async () => {
|
||||
const mockSession = {
|
||||
object: 'account_session',
|
||||
client_secret: 'acct_sess_secret_123',
|
||||
expires_at: Date.now() + 3600
|
||||
};
|
||||
|
||||
mockStripeAccountSessionsCreate.mockResolvedValue(mockSession);
|
||||
|
||||
const result = await StripeService.createAccountSession('acct_123456789');
|
||||
|
||||
expect(mockStripeAccountSessionsCreate).toHaveBeenCalledWith({
|
||||
account: 'acct_123456789',
|
||||
components: {
|
||||
account_onboarding: { enabled: true }
|
||||
}
|
||||
});
|
||||
expect(result).toEqual(mockSession);
|
||||
});
|
||||
|
||||
it('should handle account session creation errors', async () => {
|
||||
const stripeError = new Error('Account not found');
|
||||
mockStripeAccountSessionsCreate.mockRejectedValue(stripeError);
|
||||
|
||||
await expect(StripeService.createAccountSession('invalid_account'))
|
||||
.rejects.toThrow('Account not found');
|
||||
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Error creating account session',
|
||||
expect.objectContaining({
|
||||
error: stripeError.message,
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle invalid account ID', async () => {
|
||||
const stripeError = new Error('Invalid account ID format');
|
||||
mockStripeAccountSessionsCreate.mockRejectedValue(stripeError);
|
||||
|
||||
await expect(StripeService.createAccountSession(null))
|
||||
.rejects.toThrow('Invalid account ID format');
|
||||
});
|
||||
});
|
||||
|
||||
describe('getPaymentMethod', () => {
|
||||
it('should retrieve payment method successfully', async () => {
|
||||
const mockPaymentMethod = {
|
||||
id: 'pm_123456789',
|
||||
type: 'card',
|
||||
card: {
|
||||
brand: 'visa',
|
||||
last4: '4242',
|
||||
exp_month: 12,
|
||||
exp_year: 2025
|
||||
},
|
||||
customer: 'cus_123456789'
|
||||
};
|
||||
|
||||
mockStripePaymentMethodsRetrieve.mockResolvedValue(mockPaymentMethod);
|
||||
|
||||
const result = await StripeService.getPaymentMethod('pm_123456789');
|
||||
|
||||
expect(mockStripePaymentMethodsRetrieve).toHaveBeenCalledWith('pm_123456789');
|
||||
expect(result).toEqual(mockPaymentMethod);
|
||||
});
|
||||
|
||||
it('should handle payment method retrieval errors', async () => {
|
||||
const stripeError = new Error('Payment method not found');
|
||||
mockStripePaymentMethodsRetrieve.mockRejectedValue(stripeError);
|
||||
|
||||
await expect(StripeService.getPaymentMethod('pm_invalid'))
|
||||
.rejects.toThrow('Payment method not found');
|
||||
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Error retrieving payment method',
|
||||
expect.objectContaining({
|
||||
error: stripeError.message,
|
||||
paymentMethodId: 'pm_invalid'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle null payment method ID', async () => {
|
||||
const stripeError = new Error('Invalid payment method ID');
|
||||
mockStripePaymentMethodsRetrieve.mockRejectedValue(stripeError);
|
||||
|
||||
await expect(StripeService.getPaymentMethod(null))
|
||||
.rejects.toThrow('Invalid payment method ID');
|
||||
});
|
||||
});
|
||||
|
||||
describe('isAccountDisconnectedError', () => {
|
||||
it('should return true for account_invalid error code', () => {
|
||||
const error = { code: 'account_invalid', message: 'Account is invalid' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for platform_api_key_expired error code', () => {
|
||||
const error = { code: 'platform_api_key_expired', message: 'API key expired' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for error message containing "cannot transfer"', () => {
|
||||
const error = { code: 'some_code', message: 'You cannot transfer to this account' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for error message containing "not connected"', () => {
|
||||
const error = { code: 'some_code', message: 'This account is not connected to your platform' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for error message containing "no longer connected"', () => {
|
||||
const error = { code: 'some_code', message: 'This account is no longer connected' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return true for error message containing "account has been deauthorized"', () => {
|
||||
const error = { code: 'some_code', message: 'The account has been deauthorized' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(true);
|
||||
});
|
||||
|
||||
it('should return false for unrelated error codes', () => {
|
||||
const error = { code: 'card_declined', message: 'Card was declined' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should return false for unrelated error messages', () => {
|
||||
const error = { code: 'some_code', message: 'Insufficient funds in account' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle error with no message', () => {
|
||||
const error = { code: 'some_code' };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle error with undefined message', () => {
|
||||
const error = { code: 'some_code', message: undefined };
|
||||
expect(StripeService.isAccountDisconnectedError(error)).toBe(false);
|
||||
});
|
||||
});
|
||||
|
||||
describe('handleDisconnectedAccount', () => {
|
||||
beforeEach(() => {
|
||||
mockUserFindOne.mockReset();
|
||||
mockSendAccountDisconnectedEmail.mockReset();
|
||||
});
|
||||
|
||||
it('should clear user stripe connection data', async () => {
|
||||
const mockUser = {
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
update: mockUserUpdate.mockResolvedValue(true)
|
||||
};
|
||||
mockUserFindOne.mockResolvedValue(mockUser);
|
||||
mockSendAccountDisconnectedEmail.mockResolvedValue(true);
|
||||
|
||||
await StripeService.handleDisconnectedAccount('acct_123456789');
|
||||
|
||||
expect(mockUserFindOne).toHaveBeenCalledWith({
|
||||
where: { stripeConnectedAccountId: 'acct_123456789' }
|
||||
});
|
||||
expect(mockUserUpdate).toHaveBeenCalledWith({
|
||||
stripeConnectedAccountId: null,
|
||||
stripePayoutsEnabled: false
|
||||
});
|
||||
});
|
||||
|
||||
it('should send account disconnected email', async () => {
|
||||
const mockUser = {
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
update: mockUserUpdate.mockResolvedValue(true)
|
||||
};
|
||||
mockUserFindOne.mockResolvedValue(mockUser);
|
||||
mockSendAccountDisconnectedEmail.mockResolvedValue(true);
|
||||
|
||||
await StripeService.handleDisconnectedAccount('acct_123456789');
|
||||
|
||||
expect(mockSendAccountDisconnectedEmail).toHaveBeenCalledWith('test@example.com', {
|
||||
ownerName: 'John',
|
||||
hasPendingPayouts: true,
|
||||
pendingPayoutCount: 1
|
||||
});
|
||||
});
|
||||
|
||||
it('should do nothing when user not found', async () => {
|
||||
mockUserFindOne.mockResolvedValue(null);
|
||||
|
||||
await StripeService.handleDisconnectedAccount('acct_nonexistent');
|
||||
|
||||
expect(mockUserFindOne).toHaveBeenCalledWith({
|
||||
where: { stripeConnectedAccountId: 'acct_nonexistent' }
|
||||
});
|
||||
expect(mockUserUpdate).not.toHaveBeenCalled();
|
||||
expect(mockSendAccountDisconnectedEmail).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should handle email sending errors gracefully', async () => {
|
||||
const mockUser = {
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
update: mockUserUpdate.mockResolvedValue(true)
|
||||
};
|
||||
mockUserFindOne.mockResolvedValue(mockUser);
|
||||
mockSendAccountDisconnectedEmail.mockRejectedValue(new Error('Email service down'));
|
||||
|
||||
// Should not throw
|
||||
await expect(StripeService.handleDisconnectedAccount('acct_123456789'))
|
||||
.resolves.not.toThrow();
|
||||
|
||||
// Should have logged the error
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Failed to clean up disconnected account',
|
||||
expect.objectContaining({
|
||||
accountId: 'acct_123456789'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle user update errors gracefully', async () => {
|
||||
const mockUser = {
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
lastName: 'Doe',
|
||||
update: jest.fn().mockRejectedValue(new Error('Database error'))
|
||||
};
|
||||
mockUserFindOne.mockResolvedValue(mockUser);
|
||||
|
||||
// Should not throw
|
||||
await expect(StripeService.handleDisconnectedAccount('acct_123456789'))
|
||||
.resolves.not.toThrow();
|
||||
|
||||
expect(mockLoggerError).toHaveBeenCalledWith(
|
||||
'Failed to clean up disconnected account',
|
||||
expect.objectContaining({
|
||||
accountId: 'acct_123456789'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should use lastName as fallback when firstName is not available', async () => {
|
||||
const mockUser = {
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
firstName: null,
|
||||
lastName: 'Doe',
|
||||
update: mockUserUpdate.mockResolvedValue(true)
|
||||
};
|
||||
mockUserFindOne.mockResolvedValue(mockUser);
|
||||
mockSendAccountDisconnectedEmail.mockResolvedValue(true);
|
||||
|
||||
await StripeService.handleDisconnectedAccount('acct_123456789');
|
||||
|
||||
expect(mockSendAccountDisconnectedEmail).toHaveBeenCalledWith('test@example.com', {
|
||||
ownerName: 'Doe',
|
||||
hasPendingPayouts: true,
|
||||
pendingPayoutCount: 1
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('createTransfer - disconnected account handling', () => {
|
||||
beforeEach(() => {
|
||||
mockUserFindOne.mockReset();
|
||||
mockSendAccountDisconnectedEmail.mockReset();
|
||||
mockLoggerWarn.mockReset();
|
||||
});
|
||||
|
||||
it('should call handleDisconnectedAccount when account_invalid error occurs', async () => {
|
||||
const disconnectedError = new Error('The account has been deauthorized');
|
||||
disconnectedError.code = 'account_invalid';
|
||||
mockStripeTransfersCreate.mockRejectedValue(disconnectedError);
|
||||
|
||||
const mockUser = {
|
||||
id: 123,
|
||||
email: 'test@example.com',
|
||||
firstName: 'John',
|
||||
update: mockUserUpdate.mockResolvedValue(true)
|
||||
};
|
||||
mockUserFindOne.mockResolvedValue(mockUser);
|
||||
mockSendAccountDisconnectedEmail.mockResolvedValue(true);
|
||||
|
||||
await expect(StripeService.createTransfer({
|
||||
amount: 50.00,
|
||||
destination: 'acct_disconnected'
|
||||
})).rejects.toThrow('The account has been deauthorized');
|
||||
|
||||
// Wait for async handleDisconnectedAccount to complete
|
||||
await new Promise(resolve => setTimeout(resolve, 10));
|
||||
|
||||
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||
'Transfer failed - account appears disconnected',
|
||||
expect.objectContaining({
|
||||
destination: 'acct_disconnected',
|
||||
errorCode: 'account_invalid'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should still throw the original error after cleanup', async () => {
|
||||
const disconnectedError = new Error('Cannot transfer to this account');
|
||||
disconnectedError.code = 'account_invalid';
|
||||
mockStripeTransfersCreate.mockRejectedValue(disconnectedError);
|
||||
|
||||
mockUserFindOne.mockResolvedValue(null);
|
||||
|
||||
await expect(StripeService.createTransfer({
|
||||
amount: 50.00,
|
||||
destination: 'acct_disconnected'
|
||||
})).rejects.toThrow('Cannot transfer to this account');
|
||||
});
|
||||
|
||||
it('should log warning for disconnected account errors', async () => {
|
||||
const disconnectedError = new Error('This account is no longer connected');
|
||||
disconnectedError.code = 'some_error';
|
||||
disconnectedError.type = 'StripeInvalidRequestError';
|
||||
mockStripeTransfersCreate.mockRejectedValue(disconnectedError);
|
||||
|
||||
mockUserFindOne.mockResolvedValue(null);
|
||||
|
||||
await expect(StripeService.createTransfer({
|
||||
amount: 50.00,
|
||||
destination: 'acct_disconnected'
|
||||
})).rejects.toThrow('This account is no longer connected');
|
||||
|
||||
expect(mockLoggerWarn).toHaveBeenCalledWith(
|
||||
'Transfer failed - account appears disconnected',
|
||||
expect.objectContaining({
|
||||
destination: 'acct_disconnected'
|
||||
})
|
||||
);
|
||||
});
|
||||
|
||||
it('should not call handleDisconnectedAccount for non-disconnection errors', async () => {
|
||||
const normalError = new Error('Insufficient balance');
|
||||
normalError.code = 'insufficient_balance';
|
||||
mockStripeTransfersCreate.mockRejectedValue(normalError);
|
||||
|
||||
await expect(StripeService.createTransfer({
|
||||
amount: 50.00,
|
||||
destination: 'acct_123'
|
||||
})).rejects.toThrow('Insufficient balance');
|
||||
|
||||
expect(mockLoggerWarn).not.toHaveBeenCalledWith(
|
||||
'Transfer failed - account appears disconnected',
|
||||
expect.any(Object)
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
describe('chargePaymentMethod - additional cases', () => {
|
||||
it('should handle authentication_required error and return requires_action', async () => {
|
||||
const authError = new Error('Authentication required');
|
||||
authError.code = 'authentication_required';
|
||||
authError.payment_intent = {
|
||||
id: 'pi_requires_auth',
|
||||
client_secret: 'pi_requires_auth_secret'
|
||||
};
|
||||
mockStripePaymentIntentsCreate.mockRejectedValue(authError);
|
||||
|
||||
const result = await StripeService.chargePaymentMethod(
|
||||
'pm_123',
|
||||
50.00,
|
||||
'cus_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('requires_action');
|
||||
expect(result.requiresAction).toBe(true);
|
||||
expect(result.paymentIntentId).toBe('pi_requires_auth');
|
||||
expect(result.clientSecret).toBe('pi_requires_auth_secret');
|
||||
});
|
||||
|
||||
it('should handle us_bank_account payment method type', async () => {
|
||||
const mockPaymentIntent = {
|
||||
id: 'pi_bank',
|
||||
status: 'succeeded',
|
||||
client_secret: 'pi_bank_secret',
|
||||
created: Date.now() / 1000,
|
||||
latest_charge: {
|
||||
payment_method_details: {
|
||||
type: 'us_bank_account',
|
||||
us_bank_account: {
|
||||
last4: '6789',
|
||||
bank_name: 'Test Bank'
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockStripePaymentIntentsCreate.mockResolvedValue(mockPaymentIntent);
|
||||
|
||||
const result = await StripeService.chargePaymentMethod(
|
||||
'pm_bank_123',
|
||||
50.00,
|
||||
'cus_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('succeeded');
|
||||
expect(result.paymentMethod).toEqual({
|
||||
type: 'bank',
|
||||
brand: 'bank_account',
|
||||
last4: '6789'
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle unknown payment method type', async () => {
|
||||
const mockPaymentIntent = {
|
||||
id: 'pi_unknown',
|
||||
status: 'succeeded',
|
||||
client_secret: 'pi_unknown_secret',
|
||||
created: Date.now() / 1000,
|
||||
latest_charge: {
|
||||
payment_method_details: {
|
||||
type: 'crypto_wallet'
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockStripePaymentIntentsCreate.mockResolvedValue(mockPaymentIntent);
|
||||
|
||||
const result = await StripeService.chargePaymentMethod(
|
||||
'pm_crypto_123',
|
||||
50.00,
|
||||
'cus_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('succeeded');
|
||||
expect(result.paymentMethod).toEqual({
|
||||
type: 'crypto_wallet',
|
||||
brand: 'crypto_wallet',
|
||||
last4: null
|
||||
});
|
||||
});
|
||||
|
||||
it('should handle payment with no charge details', async () => {
|
||||
const mockPaymentIntent = {
|
||||
id: 'pi_no_charge',
|
||||
status: 'succeeded',
|
||||
client_secret: 'pi_no_charge_secret',
|
||||
created: Date.now() / 1000,
|
||||
latest_charge: null
|
||||
};
|
||||
|
||||
mockStripePaymentIntentsCreate.mockResolvedValue(mockPaymentIntent);
|
||||
|
||||
const result = await StripeService.chargePaymentMethod(
|
||||
'pm_123',
|
||||
50.00,
|
||||
'cus_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('succeeded');
|
||||
expect(result.paymentMethod).toBeNull();
|
||||
});
|
||||
|
||||
it('should handle card with missing details gracefully', async () => {
|
||||
const mockPaymentIntent = {
|
||||
id: 'pi_card_no_details',
|
||||
status: 'succeeded',
|
||||
client_secret: 'pi_card_secret',
|
||||
created: Date.now() / 1000,
|
||||
latest_charge: {
|
||||
payment_method_details: {
|
||||
type: 'card',
|
||||
card: {} // Missing brand and last4
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
mockStripePaymentIntentsCreate.mockResolvedValue(mockPaymentIntent);
|
||||
|
||||
const result = await StripeService.chargePaymentMethod(
|
||||
'pm_123',
|
||||
50.00,
|
||||
'cus_123'
|
||||
);
|
||||
|
||||
expect(result.status).toBe('succeeded');
|
||||
expect(result.paymentMethod).toEqual({
|
||||
type: 'card',
|
||||
brand: 'card',
|
||||
last4: '****'
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user