mirror of
https://github.com/Tria-plc/edr-platform.git
synced 2026-08-26 18:42:49 +00:00
81 lines
3.3 KiB
JavaScript
81 lines
3.3 KiB
JavaScript
// Minimal payment-service stand-in for the e2e stack.
|
|
//
|
|
// WHY THIS EXISTS
|
|
//
|
|
// `expireBooking` will not expire an unpaid reservation until it has asked the
|
|
// payment gateway whether the money landed late — reconcile-before-expire
|
|
// (booking-batch.service.ts:3484-3506). Any error answering that question is
|
|
// treated as `unverifiable: true`, and an unverifiable answer DEFERS the
|
|
// expiry rather than risk expiring a customer who actually paid:
|
|
//
|
|
// [BATCH] expire deferred for BK-… — settlement unverifiable at the
|
|
// gateway; retrying next settle tick
|
|
//
|
|
// That is correct in production. In e2e there is no payment microservice, and
|
|
// PAYMENT_API_URL defaults to the real https://paymentcallback.triaplc.com
|
|
// (payment-client.service.ts:25), so every reconcile call fails and EVERY
|
|
// unpaid hold defers forever. Six scenarios turn on a reservation expiring
|
|
// (G1·S1, G1·S5, G1·S6, G3·S16, G5·S22, G5·S24), and all of them hang on it.
|
|
//
|
|
// This server answers the two calls that path makes, so the engine gets a
|
|
// definite "no payment exists" and expires the hold as designed. It is
|
|
// deliberately dumb: nothing here simulates a real gateway, and the specs that
|
|
// need a SUCCESSFUL payment do not come through here at all — they deliver
|
|
// `payment.succeeded` to the API's own internal webhook (see settleViaGateway
|
|
// in import-utils.ts), which is the real production path for a settled
|
|
// payment.
|
|
const http = require("node:http");
|
|
|
|
const PORT = process.env.PORT || 4500;
|
|
|
|
/**
|
|
* `paid: false, unverifiable: false` = "the gateway is reachable and holds no
|
|
* settled payment for this reference". That is the answer that lets an expiry
|
|
* proceed. Returning `unverifiable: true` here would reproduce the exact
|
|
* deadlock this mock exists to remove.
|
|
*/
|
|
const NOT_PAID = { paid: false, unverifiable: false };
|
|
|
|
const server = http.createServer((req, res) => {
|
|
let body = "";
|
|
req.on("data", (chunk) => {
|
|
body += chunk;
|
|
});
|
|
req.on("end", () => {
|
|
const send = (status, payload) => {
|
|
const json = JSON.stringify(payload);
|
|
res.writeHead(status, {
|
|
"content-type": "application/json",
|
|
"content-length": Buffer.byteLength(json),
|
|
});
|
|
res.end(json);
|
|
};
|
|
|
|
// POST /payments/reconcile — the reconcile-before-expire call.
|
|
if (req.method === "POST" && req.url.startsWith("/payments/reconcile")) {
|
|
console.log(`[payment-mock] reconcile ${body || "(no body)"} → not paid`);
|
|
return send(200, NOT_PAID);
|
|
}
|
|
|
|
// GET /payments/intents?… — the intent lookup. 404 is a valid "no intent
|
|
// for this reference" answer and the client maps it to null rather than
|
|
// treating it as an error (payment-client.service.ts:72-73).
|
|
if (req.method === "GET" && req.url.startsWith("/payments/intents")) {
|
|
console.log(`[payment-mock] intents ${req.url} → 404 (none)`);
|
|
return send(404, { message: "No intent for this reference" });
|
|
}
|
|
|
|
// Health probe for the compose healthcheck.
|
|
if (req.method === "GET" && req.url.startsWith("/health")) {
|
|
return send(200, { ok: true });
|
|
}
|
|
|
|
console.log(`[payment-mock] unhandled ${req.method} ${req.url}`);
|
|
send(404, { message: `Unhandled ${req.method} ${req.url}` });
|
|
});
|
|
});
|
|
|
|
server.listen(PORT, () => {
|
|
console.log(`payment-mock listening on ${PORT}`);
|
|
});
|