taler-docs

Documentation for GNU Taler components, APIs and protocols
Log | Files | Refs | README | LICENSE

public-orders-get.ts (6247B)


      1 // Merchant's DB state for one particular order.
      2 // Invariants:
      3 // paid => claimed
      4 // claimed => !!contractHash
      5 // requireClaimToken => !!claimToken
      6 // !!lastPaidSessionId => paid
      7 interface MerchantOrderInfo {
      8   orderId: string;
      9   requireClaimToken: boolean;
     10   claimToken?: string;
     11   contractHash?: string;
     12   claimed: boolean;
     13   paid: boolean;
     14   // Refund hasn't been picked up yet
     15   refundPending: boolean;
     16   fulfillmentUrl?: string;
     17   publicReorderUrl?: string;
     18   lastPaidSessionId?: string;
     19 }
     20 
     21 // Data from the client's request to /orders/{id}
     22 interface Req {
     23   orderId: string;
     24   contractHash?: string;
     25   claimToken?: string;
     26   sessionId?: string;
     27   accept: "json" | "html";
     28 }
     29 
     30 // (Abstract) response to /orders/{id}
     31 interface Resp {
     32   httpStatus: string;
     33   contentType: "json" | "html";
     34   // Schema type of the response
     35   responseType: string;
     36   // Redirect "Location: " if applicable to status code
     37   redirectLocation?: string;
     38   // "Taler: " header in the response
     39   talerHeader?: string;
     40   // Additional details about response
     41   response?: any;
     42 }
     43 
     44 // Abstracted merchant database
     45 type MerchantOrderStore = { [orderId: string]: MerchantOrderInfo };
     46 
     47 // Logic for /orders/{id}
     48 function handlePublicOrdersGet(mos: MerchantOrderStore, req: Req): Resp {
     49   const ord = mos[req.orderId];
     50   if (!ord) {
     51     return respNotFound(req);
     52   }
     53 
     54   const authMissing = !!req.contractHash && !!req.claimToken;
     55   // For this endpoint, when the order does not have a claim token,
     56   // the order status can be accessed *without* h_contract.
     57   const authOk =
     58     ord.contractHash === req.contractHash ||
     59     (ord.requireClaimToken && ord.claimToken === req.claimToken) ||
     60     !ord.requireClaimToken;
     61 
     62   if (authMissing && ord.requireClaimToken) {
     63     // Client is trying to get the order status of an
     64     // order.  However, the client is not showing authentication.
     65     //
     66     // This can happen when the fulfillment URL includes the order ID,
     67     // and the storefront redirects the user to the backend QR code
     68     // page, because the order is not paid under the current session.
     69     // This happens on bookmarking / link sharing.
     70     if (!ord.publicReorderUrl) {
     71       return respForbidden(req);
     72     }
     73     return respGoto(req, ord.publicReorderUrl);
     74   }
     75 
     76   // Even if an order is paid for,
     77   // we still need the ord.claimToken, because
     78   // the QR code page will poll until it gets a
     79   // fulfillment URL, but we decided that the
     80   // fulfillment URL should only be returned
     81   // when the client is authenticated.
     82   // (Otherwise, guessing the order ID might leak the
     83   // fulfillment URL).
     84   if (!authOk) {
     85     return respForbidden(req);
     86   }
     87 
     88   if (!!req.sessionId && req.sessionId !== ord.lastPaidSessionId) {
     89     if (!!ord.fulfillmentUrl) {
     90       const alreadyPaidOrd = findAlreadyPaid(
     91         mos,
     92         req.sessionId,
     93         ord.fulfillmentUrl
     94       );
     95       if (!!alreadyPaidOrd) {
     96         return respAlreadyPaid(req, alreadyPaidOrd);
     97       }
     98     }
     99   }
    100 
    101   if (ord.paid) {
    102     return respPaid(req, ord);
    103   }
    104 
    105   return respUnpaid(req, ord);
    106 }
    107 
    108 function respNotFound(req: Req): Resp {
    109   return {
    110     contentType: req.accept,
    111     httpStatus: "404 Not Found",
    112     responseType: "TalerError",
    113   };
    114 }
    115 
    116 function respForbidden(req: Req): Resp {
    117   return {
    118     contentType: req.accept,
    119     httpStatus: "403 Forbidden",
    120     responseType: "TalerError",
    121   };
    122 }
    123 
    124 function respAlreadyPaid(req: Req, alreadyPaidOrd: MerchantOrderInfo): Resp {
    125   // This could be called with an empty fulfillment URL, but that doesn't
    126   // really make sense for the client's perspective.
    127   if (req.accept === "html") {
    128     return {
    129       httpStatus: "302 Found",
    130       contentType: "html",
    131       redirectLocation: alreadyPaidOrd.fulfillmentUrl,
    132       responseType: "empty",
    133     };
    134   }
    135   return {
    136     httpStatus: "402 PaymentRequired",
    137     contentType: "json",
    138     responseType: "StatusUnpaidResponse",
    139     response: {
    140       fulfillment_url: alreadyPaidOrd.fulfillmentUrl,
    141       already_paid_order_id: alreadyPaidOrd.orderId,
    142     },
    143   };
    144 }
    145 
    146 function respGoto(req: Req, publicReorderUrl: string): Resp {
    147   if (req.accept === "html") {
    148     return {
    149       httpStatus: "302 Found",
    150       contentType: "html",
    151       redirectLocation: publicReorderUrl,
    152       responseType: "empty",
    153     };
    154   }
    155   return {
    156     httpStatus: "202 Accepted",
    157     contentType: "json",
    158     responseType: "StatusGotoResponse",
    159     response: {
    160       public_reorder_url: publicReorderUrl,
    161     },
    162   };
    163 }
    164 
    165 function respUnpaid(req: Req, ord: MerchantOrderInfo): Resp {
    166   if (req.accept === "html") {
    167     return {
    168       httpStatus: "402 Payment Required",
    169       contentType: "html",
    170       responseType: "StatusUnpaidResponse",
    171       // This must include the claim token.  The same taler://
    172       // URI should also be shown as the QR code.
    173       talerHeader: "taler://pay/...",
    174     };
    175   }
    176   return {
    177     httpStatus: "402 Payment Required",
    178     contentType: "json",
    179     responseType: "StatusUnpaidResponse",
    180     response: {
    181       // Required for repurchase detection
    182       fulfillmentUrl: ord.fulfillmentUrl,
    183     },
    184   };
    185 }
    186 
    187 function respPaid(req: Req, ord: MerchantOrderInfo): Resp {
    188   if (req.accept === "html") {
    189     if (ord.refundPending) {
    190       return {
    191         httpStatus: "402 Payment Required",
    192         contentType: "html",
    193         responseType: "QRCodeRefundPage",
    194         talerHeader: "taler://refund/...",
    195       };
    196     }
    197     // We do not redirect here.  Only
    198     // the JS on the QR code page automatically redirects.
    199     // Without JS, the user has to manually click through to
    200     // the fulfillment URL.
    201     return {
    202       httpStatus: "200 OK",
    203       contentType: "html",
    204       responseType: "OrderStatusHtmlPage",
    205     };
    206   }
    207   return {
    208     httpStatus: "200 OK",
    209     contentType: "json",
    210     responseType: "StatusPaidResponse",
    211     response: {
    212       fulfillmentUrl: ord.fulfillmentUrl,
    213     },
    214   };
    215 }
    216 
    217 // Helper to find an already paid order ID.
    218 function findAlreadyPaid(
    219   mos: MerchantOrderStore,
    220   sessionId: string,
    221   fulfillmentUrl: string
    222 ): MerchantOrderInfo | undefined {
    223   for (const orderId of Object.keys(mos)) {
    224     if (
    225       mos[orderId].lastPaidSessionId === sessionId &&
    226       mos[orderId].fulfillmentUrl === fulfillmentUrl
    227     ) {
    228       return mos[orderId];
    229     }
    230   }
    231   return undefined;
    232 }