'use strict'; // ── Integration test: payment.service.js → providers/registry → paypal.provider ── // Only the DB models and axios (the two real external boundaries) are mocked. // registry.js and paypal.provider.js run unmodified, so this proves the full // orchestration chain (service → registry lookup → provider → HTTP) is wired // correctly end to end, not just that each piece works in isolation. process.env.PAYPAL_CLIENT_ID = 'test-client-id'; process.env.PAYPAL_CLIENT_SECRET = 'test-client-secret'; process.env.FRONTEND_URL = 'https://app.new-starr.test'; const axios = require('axios'); jest.mock('axios'); jest.mock('../../models/tiers/payment_policies.mdl', () => ({ findOne: jest.fn() })); jest.mock('../../models/tiers/payments.mdl', () => ({ count: jest.fn() })); const mdl_PaymentPolicies = require('../../models/tiers/payment_policies.mdl'); const mdl_Payments = require('../../models/tiers/payments.mdl'); const paymentService = require('../../services/payment.service'); beforeEach(() => jest.clearAllMocks()); function mockAccessToken(token = 'tok-abc') { axios.post.mockImplementationOnce((url) => { expect(url).toMatch(/\/v1\/oauth2\/token$/); return Promise.resolve({ data: { access_token: token } }); }); } // ── Provider delegation ───────────────────────────────────────────────────────── describe('provider delegation (registry → paypal.provider → axios)', () => { test('createOrder("paypal", ...) reaches PayPal\'s create-order endpoint', async () => { mockAccessToken(); axios.post.mockResolvedValueOnce({ data: { id: 'ORDER-1', status: 'CREATED' } }); const result = await paymentService.createOrder('paypal', { amount: 25, referenceId: 'plan-1' }); expect(axios.post.mock.calls[1][0]).toBe('https://api-m.sandbox.paypal.com/v2/checkout/orders'); expect(result).toEqual({ id: 'ORDER-1', status: 'CREATED' }); }); test('captureOrder("paypal", ...) reaches PayPal\'s capture endpoint', async () => { mockAccessToken(); axios.post.mockResolvedValueOnce({ data: { id: 'ORDER-1', status: 'COMPLETED' } }); const result = await paymentService.captureOrder('paypal', 'ORDER-1'); expect(axios.post.mock.calls[1][0]).toBe('https://api-m.sandbox.paypal.com/v2/checkout/orders/ORDER-1/capture'); expect(result).toEqual({ id: 'ORDER-1', status: 'COMPLETED' }); }); test('refundCapture("paypal", ...) reaches PayPal\'s refund endpoint', async () => { mockAccessToken(); axios.post.mockResolvedValueOnce({ data: { id: 'REFUND-1', status: 'COMPLETED' } }); const result = await paymentService.refundCapture('paypal', 'CAPTURE-1', 10, 'USD'); expect(axios.post.mock.calls[1][0]).toBe('https://api-m.sandbox.paypal.com/v2/payments/captures/CAPTURE-1/refund'); expect(result).toEqual({ id: 'REFUND-1', status: 'COMPLETED' }); }); test('an unconfigured/unknown provider name fails fast with a descriptive error instead of hitting axios', () => { expect(() => paymentService.createOrder('stripe', { amount: 25, referenceId: 'plan-1' })) .toThrow(/Unknown payment provider: "stripe"/); expect(axios.post).not.toHaveBeenCalled(); }); }); // ── Refund policy ──────────────────────────────────────────────────────────────── describe('refund policy', () => { test('falls back to the 5-minute default window when no policy is configured', () => { expect(paymentService.getRefundWindowMs(null)).toBe(5 * 60_000); expect(paymentService.isRefundAllowed(null)).toBe(true); }); test('honors a custom policy window and unit', () => { const policy = { refund_policy: { allowed: false, window_value: 2, window_unit: 'hours' } }; expect(paymentService.getRefundWindowMs(policy)).toBe(2 * 3_600_000); expect(paymentService.isRefundAllowed(policy)).toBe(false); }); test('getPolicyForPlan() looks up the policy by plan_id', async () => { mdl_PaymentPolicies.findOne.mockResolvedValue({ plan_id: 7 }); const policy = await paymentService.getPolicyForPlan(7); expect(mdl_PaymentPolicies.findOne).toHaveBeenCalledWith({ where: { plan_id: 7 } }); expect(policy).toEqual({ plan_id: 7 }); }); }); // ── Promo evaluation ─────────────────────────────────────────────────────────── describe('evaluatePromo()', () => { const plan = { plan_id: 1, price: 100 }; test('rejects when no code is provided', async () => { const result = await paymentService.evaluatePromo({}, plan, ''); expect(result).toEqual({ valid: false, reason: 'No promo code provided.' }); }); test('rejects an unknown code', async () => { const policy = { promo_rules: [] }; const result = await paymentService.evaluatePromo(policy, plan, 'BOGUS'); expect(result.valid).toBe(false); expect(result.reason).toBe('Invalid promo code.'); }); test('applies a flat discount', async () => { const policy = { promo_rules: [{ code: 'FLAT10', type: 'flat', value: 10 }] }; mdl_Payments.count.mockResolvedValue(0); const result = await paymentService.evaluatePromo(policy, plan, 'flat10'); expect(result).toMatchObject({ valid: true, code: 'FLAT10', discount: 10 }); }); test('applies a percent discount capped by max_discount', async () => { const policy = { promo_rules: [{ code: 'PCT50', type: 'percent', value: 50, max_discount: 30 }] }; mdl_Payments.count.mockResolvedValue(0); const result = await paymentService.evaluatePromo(policy, plan, 'PCT50'); expect(result).toMatchObject({ valid: true, discount: 30 }); }); test('rejects an expired code', async () => { const policy = { promo_rules: [{ code: 'OLD', type: 'flat', value: 5, expires_at: '2000-01-01' }] }; const result = await paymentService.evaluatePromo(policy, plan, 'OLD'); expect(result).toMatchObject({ valid: false, reason: 'Promo code has expired.' }); }); test('rejects once max_uses has been reached', async () => { const policy = { promo_rules: [{ code: 'LIMITED', type: 'flat', value: 5, max_uses: 2 }] }; mdl_Payments.count.mockResolvedValue(2); const result = await paymentService.evaluatePromo(policy, plan, 'LIMITED'); expect(result).toMatchObject({ valid: false, reason: 'Promo code has reached its usage limit.' }); }); test('rejects when the subtotal is below min_amount', async () => { const policy = { promo_rules: [{ code: 'BIGSPEND', type: 'flat', value: 5, min_amount: 200 }] }; mdl_Payments.count.mockResolvedValue(0); const result = await paymentService.evaluatePromo(policy, plan, 'BIGSPEND'); expect(result.valid).toBe(false); expect(result.reason).toMatch(/minimum purchase/); }); test('evaluates against effectivePrice (localized currency) instead of plan.price when provided', async () => { const policy = { promo_rules: [{ code: 'PCT10', type: 'percent', value: 10 }] }; mdl_Payments.count.mockResolvedValue(0); const result = await paymentService.evaluatePromo(policy, plan, 'PCT10', 50); expect(result.discount).toBe(5); }); });