All files / core/semantic-log/flow payee.ts

100% Statements 81/81
100% Branches 15/15
100% Functions 2/2
100% Lines 81/81

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 821x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 1x 27x 27x 27x 67x 67x 67x 67x 67x 67x 67x 67x 67x 67x 67x 67x 27x 27x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 66x 27x 27x 66x 66x 66x 66x 66x 66x 1x 1x 1x 1x 66x 2x 2x 2x 63x 63x 63x 66x 66x 66x 66x 27x  
/**
 * Payee DFSP — the far end.
 *
 * The last participant in the chain; its failure is the one that must be
 * attributed as the origin (PRD R15).
 *
 * Both faults here are deployment properties the participant is installed with
 * (`blockTransfers`, `stallTransfers`) rather than branches a test steers it
 * into: the file still reads like a real payee that happens to be configured
 * that way (Plan 3 decision 2).
 *
 * The status is set on the reply explicitly; a returned `{status, body}` object
 * would be answered with HTTP 200 by fastify, so a refusal could never reach the
 * hub and R15's origin would be the hub's own timeout instead of this service
 * (see `payer.ts` for the full note).
 */
 
import type {Participant} from './participant.ts';
 
export interface PayeeOptions {
    /** Refuse every transfer, as a blocked account would (fault F1). */
    blockTransfers?: boolean;
    /** Never answer the transfer, as a hung downstream would (fault F4). */
    stallTransfers?: boolean;
    stallMs?: number;
}
 
export function installPayee(participant: Participant, options: PayeeOptions = {}): void {
    const {logger, app} = participant;
 
    app.post('/parties', async (request, reply) => {
        const traceId = participant.traceFrom(request);
        const flowId = participant.flowFrom(request);
        const leg = participant.legFrom(request);
        const result = await participant.run(traceId, flowId, leg, () =>
            participant.phase('discovery', async () => {
                logger.info('party profile returned', {currency: 'EUR'});
                return {status: 200, body: {currency: 'EUR'}};
            }),
        );
        reply.status(result.status);
        return result.body;
    });
 
    app.post('/quotes', async (request, reply) => {
        const traceId = participant.traceFrom(request);
        const flowId = participant.flowFrom(request);
        const leg = participant.legFrom(request);
        const result = await participant.run(traceId, flowId, leg, () =>
            participant.phase('quote', async () => {
                logger.info('quote signed', {condition: 'sha256:condition'});
                return {status: 200, body: {condition: 'sha256:condition'}};
            }),
        );
        reply.status(result.status);
        return result.body;
    });
 
    app.post('/transfers', async (request, reply) => {
        const traceId = participant.traceFrom(request);
        const flowId = participant.flowFrom(request);
        const leg = participant.legFrom(request);
        const result = await participant.run(traceId, flowId, leg, () =>
            participant.phase('transfer', async () => {
                if (options.stallTransfers) {
                    logger.warn('transfer awaiting fulfilment', {waitedMs: options.stallMs ?? 50});
                    await new Promise(resolve => setTimeout(resolve, options.stallMs ?? 50));
                    return {status: 504, body: {reason: 'no fulfilment'}};
                }
                if (options.blockTransfers) {
                    logger.error('transfer refused', {err: {message: 'account blocked'}});
                    return {status: 422, body: {reason: 'account blocked'}};
                }
                logger.info('transfer fulfilled', {res: {status: 200}});
                return {status: 200, body: {fulfilment: 'sha256:preimage'}};
            }),
        );
        reply.status(result.status);
        return result.body;
    });
}