Skip to content

Add cross-language wire fixtures for ECPay - #11

Merged
linyiru merged 3 commits into
mainfrom
ecpay-wire-fixtures
Aug 10, 2026
Merged

Add cross-language wire fixtures for ECPay#11
linyiru merged 3 commits into
mainfrom
ecpay-wire-fixtures

Conversation

@linyiru

@linyiru linyiru commented Aug 10, 2026

Copy link
Copy Markdown
Contributor

What

Language-neutral JSON fixtures under fixtures/ecpay/ that pin ECPay's wire contract — given a unified SDK input, the payload the provider actually receives — plus the RtnMsg → normalized-error mapping.

file cases pins
issue.json 5 B2C member carrier, tax types (TAXABLE/ZERO_RATED/TAX_FREE → 1/2/3), B2B 統編 (CustomerIdentifier + Print=1), item remark presence
void-allowance.json 4 void, allowance (+ email-notify variant), voidAllowance
query.json 2 query by orderId (exact payload), query by invoiceNumber
errors.json 16 keyword→code and keyword→reason two-axis error mapping

Why

This is the mechanism that keeps the TypeScript SDK and the planned Ruby port twins. Both run the same fixtures through their own adapter; a wire-mapping drift on either side turns a shared fixture red. It's the working proof behind the "cross-language wire fixtures" point in the einvoice 2.0 RFC (abookyun/einvoice#31), and it lives here in our repo, independent of that discussion.

Not just well-formed — behavior-verified

packages/einvoice-ecpay/src/__tests__/fixtures.test.ts is the TypeScript consumer: it drives the real ecpay adapter through a captured request and asserts each fixture (payload and endpoint), and checks errors.json against mapEcpayError/ecpayErrorReason. 27 cases, all green. Running the fixtures against the live adapter caught three wrong hand-written guesses in errors.json (e.g. 系統忙碌 is PROVIDER not VALIDATION; 該折讓單已作廢過 carries reason already_voided) — all corrected to match verified behavior.

fixtures/README.md documents the case schema, the consumption steps, and how dynamic fields (timestamps, defaulted dates) are kept deterministic.

Scope

Purely additive — new files only. No change to the ecpay adapter or any other package.

Language-neutral JSON fixtures under fixtures/ecpay/ that pin the ECPay
wire contract — given a unified SDK input, the payload the provider
receives — plus the RtnMsg→normalized-error mapping. The Ruby port will
run the same fixtures through its own adapter, so a wire-mapping drift on
either side turns a shared fixture red.

fixtures.test.ts is the TypeScript consumer: it drives the real ecpay
adapter (issue/void/allowance/voidAllowance/query) through a captured
request and asserts each fixture, and checks errors.json against
mapEcpayError/ecpayErrorReason. 27 cases, all green.

fixtures/README.md documents the case schema, the consumption steps, and
how dynamic fields (timestamps, defaulted dates) are kept deterministic.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Adds language-neutral JSON fixtures under fixtures/ecpay/ to pin ECPay’s wire-level request payloads and error normalization contract, plus a Vitest consumer that drives the real TypeScript ECPay adapter against those fixtures.

Changes:

  • Add ECPay wire-contract fixtures for issue, void/allowance/voidAllowance, and query operations.
  • Add ECPay error mapping fixtures (RtnMsg/RtnCode → normalized code + reason).
  • Add TypeScript test harness that captures outbound encrypted requests via MSW and asserts fixture expectations; document the fixture schema and consumption flow.

Reviewed changes

Copilot reviewed 6 out of 6 changed files in this pull request and generated 4 comments.

Show a summary per file
File Description
packages/einvoice-ecpay/src/tests/fixtures.test.ts New MSW-backed test that consumes fixtures/ecpay/* and asserts captured wire payloads + error mapping outputs.
fixtures/README.md Documentation for fixture layout, schema, and how SDKs should consume fixtures.
fixtures/ecpay/issue.json Operation fixtures for issue wire payload mapping cases.
fixtures/ecpay/void-allowance.json Operation fixtures for void, allowance, and voidAllowance wire payload mapping cases.
fixtures/ecpay/query.json Operation fixtures for query wire payload mapping cases, including an exact-match case.
fixtures/ecpay/errors.json Error mapping fixtures asserting normalized code and reason derived from ECPay messages.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment on lines +67 to +89
async function capture(c: WireCase) {
let data: Record<string, unknown> | undefined;
server.use(
http.post(`${BASE}${ENDPOINT_FOR[c.operation]}`, async ({ request }) => {
data = parseRequest(await request.text()).data;
return HttpResponse.json(ecSuccess(RESULT_STUB[c.operation]));
}),
);
const provider = testProvider();
// eslint-disable-next-line @typescript-eslint/no-explicit-any
await (provider as any)[c.operation](c.input);
return data as Record<string, unknown>;
}

for (const file of ["issue.json", "void-allowance.json", "query.json"]) {
describe(`fixtures/ecpay/${file}`, () => {
for (const c of load(file)) {
it(c.name, async () => {
const data = await capture(c);
expect(data).toMatchObject(c.expect.data);
if (c.expect.dataExact) {
expect(Object.keys(data).sort()).toEqual(Object.keys(c.expect.data).sort());
}
Comment on lines +4 to +6
"note": "ECPay classification is keyword-based on RtnMsg, not purely on RtnCode (several rows share RtnCode 0). `code` is the InvoiceErrorCode; `reason` is the optional action-oriented axis (null when the message implies no distinct consumer action). Both SDKs must agree on both axes.",
"cases": [
{
Comment thread fixtures/README.md Outdated
Comment on lines +82 to +83
`code` is the stable `InvoiceErrorCode` both SDKs normalize onto; `rawCode` is
the provider code preserved on the error.
Comment thread fixtures/README.md Outdated
Address review feedback on the fixtures:

- The consumer registered its MSW handler at a hard-coded endpoint map and
  never used the fixture's own `expect.endpoint`, so the endpoint half of
  the contract was unverified — a wrong path in a fixture would pass. Register
  the handler at `expect.endpoint` instead, so a wrong endpoint becomes an
  unhandled request and fails the case (verified by mutation). Drops the now
  redundant ENDPOINT_FOR map.
- README's error-schema example and prose showed `rawCode`, but errors.json
  and the consumer assert the two-axis `code` + `reason` contract. Correct the
  docs so other SDKs implement the right shape.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated 1 comment.

Suppressed comments (3)

packages/einvoice-ecpay/src/tests/fixtures.test.ts:16

  • FIXTURE_DIR is converted to a path and then stitched back into a file:// URL (file://${FIXTURE_DIR}), which produces a non-canonical file:////... base and is easy to get wrong across platforms. Keeping it as a URL and resolving fixture files via new URL(file, FIXTURE_DIR) is simpler and avoids path→URL pitfalls.
const FIXTURE_DIR = fileURLToPath(new URL("../../../../fixtures/ecpay/", import.meta.url));

packages/einvoice-ecpay/src/tests/fixtures.test.ts:105

  • Use the same FIXTURE_DIR URL base when reading errors.json to avoid duplicating the path→file URL construction logic.
  const cases: ErrorCase[] = JSON.parse(
    readFileSync(new URL("errors.json", `file://${FIXTURE_DIR}`), "utf8"),
  ).cases;

fixtures/README.md:43

  • The documented case schema omits itemsAbsent, but the fixtures and consumer test support it (used in fixtures/ecpay/issue.json). This makes it easy for other SDKs to miss an important assertion type when implementing the fixture runner.
    "dataAbsent": ["CustomerIdentifier"], // keys that must NOT be present (optional)
    "dataExact": false               // when true, `data` is a FULL match, not a subset
  }

Comment thread packages/einvoice-ecpay/src/__tests__/fixtures.test.ts Outdated
Keep FIXTURE_DIR as a URL and pass it straight to readFileSync via
new URL(file, FIXTURE_DIR), instead of round-tripping URL → path → URL
through fileURLToPath and file:// string concatenation. Drops the
node:url import. No behavior change (27 fixture cases still green).

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Copilot reviewed 6 out of 6 changed files in this pull request and generated no new comments.

Suppressed comments (1)

packages/einvoice-ecpay/src/tests/fixtures.test.ts:72

  • This file introduces the only // eslint-disable-next-line @typescript-eslint/no-explicit-any directive in the repo; since the project lints with oxlint (not typescript-eslint), this directive is at best misleading and may not suppress anything. You can avoid both the directive and any here by casting the provider to a per-operation callable record before indexing by c.operation.
  const provider = testProvider();
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
  await (provider as any)[c.operation](c.input);
  return data as Record<string, unknown>;

@linyiru
linyiru merged commit fcf265a into main Aug 10, 2026
5 checks passed
@linyiru
linyiru deleted the ecpay-wire-fixtures branch August 10, 2026 16:38
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants