import { buildPaypleConfig, generateOrderId, parsePaypleTimestamp, paypleAuth, paypleBilling, PaypleBillingError, payplePaymentEventDigest, payplePaymentEventId, payplePayerNumber, PaypleConfigurationError, PaypleVerificationError, resolvePaypleOrderDate, } from './payple.ts' function assert(condition: boolean, message: string): asserts condition { if (!condition) throw new Error(message) } const validConfig = { PAYPLE_ENVIRONMENT: 'live', PAYPLE_CST_ID: 'merchant-id', PAYPLE_CUST_KEY: 'merchant-secret', PAYPLE_REFUND_KEY: 'refund-secret', PAYPLE_CLIENT_KEY: 'client-key', PAYPLE_SITE_URL: 'https://d3ro.chanpaca.net/payments', } Deno.test('Payple configuration has no implicit demo credential fallback', () => { for (const missing of Object.keys(validConfig)) { const candidate = { ...validConfig, [missing]: undefined } let error: unknown try { buildPaypleConfig(candidate) } catch (caught) { error = caught } assert(error instanceof PaypleConfigurationError, `${missing} must be required`) } }) Deno.test('Payple environment selects only the official fixed API host', () => { const live = buildPaypleConfig(validConfig) assert(live.baseUrl === 'https://cpay.payple.kr', 'live host') assert(live.siteUrl === 'https://d3ro.chanpaca.net', 'referer uses registered origin') const test = buildPaypleConfig({ ...validConfig, PAYPLE_ENVIRONMENT: 'test' }) assert(test.baseUrl === 'https://democpay.payple.kr', 'test host') }) Deno.test('Payple timestamps are interpreted as Korea standard time', () => { const timestamp = parsePaypleTimestamp('20260821153045') assert(timestamp.toISOString() === '2026-08-21T06:30:45.000Z', 'KST conversion') assert( parsePaypleTimestamp('2026-08-21 15:30:45').toISOString() === timestamp.toISOString(), 'documented formatted timestamp', ) let error: unknown try { parsePaypleTimestamp('2026-99-99') } catch (caught) { error = caught } assert(error instanceof PaypleVerificationError, 'invalid timestamp must fail') }) Deno.test('Payple order identifiers are unique and carry a reconcilable payment date', () => { const first = generateOrderId('11111111-2222-4333-8444-555555555555', 'aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee') const second = generateOrderId('11111111-2222-4333-8444-555555555555', 'ffffffff-bbbb-4ccc-8ddd-eeeeeeeeeeee') assert(first !== second, 'nonce must prevent same-second collision') assert(/^[A-Za-z0-9._-]{8,64}$/.test(first), 'Payple order syntax') assert(resolvePaypleOrderDate(first) === first.slice(5, 13), 'order date is recoverable') assert(resolvePaypleOrderDate('legacy-order', '20260821153045') === '20260821', 'legacy fallback date') assert( resolvePaypleOrderDate('D3RO-20260820153045-test-nonce', '20260821003045') === '20260821', 'authoritative Payple time wins across the Korea/UTC date boundary', ) }) Deno.test('Payple payer number is deterministic, numeric and user-specific', async () => { const first = await payplePayerNumber('11111111-2222-4333-8444-555555555555') const same = await payplePayerNumber('11111111-2222-4333-8444-555555555555') const other = await payplePayerNumber('aaaaaaaa-bbbb-4ccc-8ddd-eeeeeeeeeeee') assert(/^\d{18}$/.test(first), 'payer number must satisfy Payple 18-digit contract') assert(first === same, 'same user must map consistently') assert(first !== other, 'different users must not share the fixture mapping') }) Deno.test('Payple synchronous charge and webhook share one provider event identity', async () => { const identity = { orderId: 'D3RO-20260821153045-test-nonce', payerId: 'payer-fixture', payType: 'card' as const, amount: 9900, } const directDigest = await payplePaymentEventDigest(identity) const webhookDigest = await payplePaymentEventDigest({ ...identity }) const differentCharge = await payplePaymentEventDigest({ ...identity, amount: 29900 }) assert(payplePaymentEventId(identity.orderId) === `payment:${identity.orderId}`, 'canonical event id') assert(directDigest === webhookDigest, 'same external charge must be an exact replay') assert(directDigest !== differentCharge, 'different charge identity must not collide') }) Deno.test('Payple auth response cannot redirect server calls to another origin', async () => { const originalFetch = globalThis.fetch globalThis.fetch = () => Promise.resolve(new Response(JSON.stringify({ result: 'success', cst_id: 'encrypted-id', custKey: 'encrypted-key', AuthKey: 'auth-key', PCD_PAY_HOST: 'https://cpay.payple.kr.evil.example', PCD_PAY_URL: '/php/PayChkAct.php', }), { status: 200, headers: { 'Content-Type': 'application/json' } })) let error: unknown try { await paypleAuth(buildPaypleConfig(validConfig), { payCheckFlag: true }) } catch (caught) { error = caught } finally { globalThis.fetch = originalFetch } assert( error instanceof PaypleVerificationError && error.code === 'payple_untrusted_api_host', 'untrusted Payple response host must fail closed', ) }) Deno.test('Payple billing separates definitive declines from ambiguous transport outcomes', async () => { const config = buildPaypleConfig(validConfig) const auth = { PCD_CST_ID: 'encrypted-id', PCD_CUST_KEY: 'encrypted-key', PCD_AUTH_KEY: 'auth-key', PCD_PAY_HOST: 'https://cpay.payple.kr', PCD_PAY_URL: '', } const originalFetch = globalThis.fetch const cases: Array<{ fetcher: typeof fetch definitive: boolean }> = [ { fetcher: () => Promise.reject(new TypeError('connection reset')), definitive: false, }, { fetcher: () => Promise.resolve(new Response('upstream unavailable', { status: 503 })), definitive: false, }, { fetcher: () => Promise.resolve(new Response(JSON.stringify({ PCD_PAY_RST: 'error', PCD_PAY_CODE: 'BILL0001', PCD_PAY_MSG: 'declined', }), { status: 200, headers: { 'Content-Type': 'application/json' } })), definitive: true, }, ] try { for (const testCase of cases) { globalThis.fetch = testCase.fetcher let error: unknown try { await paypleBilling(config, auth, { payerId: 'payer-fixture', amount: 9900, orderId: 'D3RO-20260821153045-test-nonce', goodsName: 'D3RO Voice Pro', }) } catch (caught) { error = caught } assert(error instanceof PaypleBillingError, 'billing failure must be typed') assert(error.definitive === testCase.definitive, 'billing certainty classification') } } finally { globalThis.fetch = originalFetch } })