new commits

Signed-off-by: Kenneth Obsequio <k80308392@gmail.com>
This commit is contained in:
2026-07-03 16:20:58 +08:00
parent 1372f4e975
commit 1d12f04967
93 changed files with 3849 additions and 1063 deletions
+159
View File
@@ -0,0 +1,159 @@
'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);
});
});