---
openapi: 3.0.1

info:
  title: JPM Webhooks API
  description: |
    Receives payment status callbacks from JP Morgan Chase and enqueues them for processing.

    JPM delivers a webhook to `POST /v1/jpm` whenever a payment status changes. The handler
    immediately validates the payload, then enqueues each `REJECTED` or `RETURNED` transaction
    as an `fmsdk.Event` to the `prod-payments-service-jpm-webhooks` SQS queue. The
    `jpm-webhook-processor` Lambda dequeues and processes each event.

    **Decoupling:** The handler does not write to DynamoDB directly. It only validates and
    enqueues. This keeps webhook acknowledgement fast and retries safe.

    **Authentication:** JPM calls this endpoint at `webhooks.floatme.io`. The endpoint is
    fronted by API Gateway. Authentication is AWS SigV4 (`Authorization` header).

    **Retry behaviour:** If enqueueing any transaction fails (SQS batch failure), the handler
    returns HTTP 500, causing JPM to retry the full webhook delivery.

    **Which events trigger downstream action:** Only transactions with `paymentStatus` of
    `RETURNED` or `REJECTED` are enqueued to SQS. All other statuses are received and
    acknowledged (HTTP 200) but not forwarded for processing.
  version: 1.0.0
  contact:
    name: FloatMe API Support
    url: https://www.floatme.io
    email: api-support@floatme.io

servers:
  - url: https://webhooks.floatme.io

tags:
  - name: webhook
    description: |
      JPM payment status callback endpoint. Receives the full webhook envelope from JPM,
      validates it, and enqueues actionable transactions to SQS for asynchronous processing.

security:
  - awsSigV4: []

components:
  securitySchemes:
    awsSigV4:
      type: apiKey
      in: header
      name: Authorization
      description: AWS Signature Version 4. JPM requests are authenticated via API Gateway.

  schemas:
    WebhookPayload:
      type: object
      description: |
        The top-level JPM webhook envelope. Contains envelope metadata and an array of
        transaction status updates (`transactionData`).

        Required fields for validation: `messageId` and `messageType`. A webhook missing
        either field returns HTTP 422.
      required:
        - messageId
        - messageType
      properties:
        messageId:
          type: string
          description: |
            Unique identifier for this webhook message assigned by JPM.
            Used for deduplication and logging.
          example: "MSG-20250115-abc123xyz"
        messageType:
          type: string
          description: |
            JPM message type identifier. Indicates the category of event being delivered.
          example: "PAYMENT_STATUS_UPDATE"
        sendTimeStamp:
          type: string
          description: ISO 8601 timestamp when JPM sent the webhook.
          example: "2025-01-15T20:00:05-05:00"
        totalNbrOfTxns:
          type: integer
          description: Total number of transactions in the `transactionData` array.
          example: 1
        transactionData:
          type: array
          description: |
            Array of transaction status updates. Each entry represents one payment whose
            status has changed. The handler only enqueues transactions with `paymentStatus`
            of `RETURNED` or `REJECTED`.
          items:
            $ref: "#/components/schemas/WebhookTransaction"

    WebhookTransaction:
      type: object
      description: |
        A single transaction status update within a JPM webhook payload.

        The `endToEndId` is the confirmation ID assigned by FloatMe at payment submission
        time and stored in the `prod-payments` DynamoDB table. The `jpm-webhook-processor`
        uses this field to look up the payment record.

        Only `RETURNED` and `REJECTED` transactions are forwarded to SQS for processing.
        All other `paymentStatus` values are silently acknowledged.
      required:
        - endToEndId
        - paymentStatus
        - processingInfo
      properties:
        endToEndId:
          type: string
          description: |
            FloatMe-assigned end-to-end payment ID. Matches the `confirmation_id` in the
            `prod-payments` DynamoDB table. Used to look up the payment record during processing.
          example: "250115ABCDE1234"
        paymentStatus:
          type: string
          description: |
            Current status of the payment as reported by JPM. Accepted as an open string —
            JPM may introduce new statuses over time and the handler silently acknowledges
            any value it does not recognise.

            Statuses currently acted upon:

            * `COMPLETED` — JPM has submitted the payment to the ACH network. Not final;
              the receiving bank may still return it. Not forwarded to SQS.
            * `REJECTED` — JPM validation failed; payment was not submitted to ACH.
              Forwarded to SQS; triggers `FAILED` status on the DynamoDB payment record.
            * `RETURNED` — The receiving bank returned the payment after ACH submission.
              Forwarded to SQS; triggers `FAILED` status on the DynamoDB payment record.

            Only `REJECTED` and `RETURNED` are enqueued to SQS. `COMPLETED` and all other
            statuses are acknowledged but not processed.
          example: "RETURNED"
        processingInfo:
          $ref: "#/components/schemas/ProcessingInfo"
        createDateTime:
          type: string
          description: ISO 8601 timestamp when JPM created this transaction record.
          example: "2025-01-15T19:55:00-05:00"
        crDrIndicator:
          type: string
          description: "`DR` for debit, `CR` for credit."
          example: "DR"
        reqExeDate:
          type: string
          description: The requested execution date for the ACH transaction (`YYYY-MM-DD`).
          example: "2025-01-15"
        valueDate:
          type: string
          description: The value date (settlement date) of the transaction.
          example: "2025-01-17"
        txnAmountCcy:
          type: string
          description: ISO 4217 currency code for `txnAmount`.
          example: "USD"
        txnAmount:
          type: number
          format: double
          description: Transaction amount in `txnAmountCcy`.
          example: 9.99
        jpmcPmtRefId:
          type: string
          description: JPM's internal payment reference ID. Stored for audit purposes.
          example: "JPMC-REF-20250115-XYZ"
        jpmcPmtBatchId:
          type: string
          description: JPM's internal batch ID for this payment.
          example: "JPMC-BATCH-20250115-001"

    ProcessingInfo:
      type: object
      description: |
        Status code and descriptive text from JPM. For `RETURNED` payments, `statusCode`
        contains JPM's ISO 20022 reason code (e.g. `AC04`). For `REJECTED` payments, it
        contains the rejection reason code.

        **The ISO `statusCode` is NOT a reliable NACHA R-code on its own.** A single ISO
        code can correspond to several distinct R-codes — e.g. across 180 days of production
        returns, `AG01` appeared as R10, R11, R20 and R29, and `NARR` as both R06 and a
        non-coded "Invalid Data". The authoritative NACHA R-code is the **leading token of
        `statusText`** (e.g. `"R10 CUST ADVISES NOT AUTH."`), which the return-code engine
        (`pkg/returncodes`) parses first. The ISO→R-code map in that package is only a
        fallback for the unambiguous ISO codes when `statusText` carries no R-code prefix.

        The `statusCode` is written to the `return_code` field and `statusText` to the
        `return_info` field on the DynamoDB payment record; the pair may trigger automatic
        blocklisting of the user.
      required:
        - statusCode
        - statusText
      properties:
        statusCode:
          type: string
          description: |
            JPM status or return code. For returned payments this is the JPM return code
            (e.g. `AC04`). For rejected payments this is the rejection reason code.
            Stored as `return_code` on the payment record.
          example: "AC04"
        statusText:
          type: string
          description: |
            Human-readable description of the status code. Stored as `return_info` on
            the payment record. For `RETURNED` ACH payments this begins with the
            authoritative NACHA R-code (e.g. `"R02 ACCOUNT CLOSED"`), which is the
            primary source the return-code engine uses to normalize the return.
            For `REJECTED` payments it is plain-English text with no R-code prefix
            (e.g. `"Invalid account number structure"`).
          example: "R02 ACCOUNT CLOSED"

paths:
  /v1/jpm:
    post:
      operationId: HandleWebhook
      summary: JPM payment status webhook
      tags:
        - webhook
      description: |
        Receives a payment status callback from JPM and enqueues actionable transactions
        to SQS for asynchronous processing by `jpm-webhook-processor`.

        **Processing steps:**
        1. Read and parse the request body as a `WebhookPayload`.
        2. Validate that `messageId` and `messageType` are present.
        3. For each transaction in `transactionData` where `paymentStatus` is `RETURNED`
           or `REJECTED`: wrap in an `fmsdk.Event` and enqueue to the
           `prod-payments-service-jpm-webhooks` SQS queue.
        4. Return HTTP 200 if all enqueue operations succeed.

        Transactions with `paymentStatus` other than `RETURNED` or `REJECTED` are silently
        ignored — the webhook is still acknowledged with HTTP 200.

        **Downstream (jpm-webhook-processor):**
        * Looks up the payment by `endToEndId` (= `confirmation_id` in DynamoDB).
        * Sets `payment_status = FAILED`, writes `return_code` and `return_info` from `processingInfo`.
        * Sets `event_type` to `SUBSCRIPTION_RETURNED` (if subscription payment) or
          `PRENOTE_RETURNED` (otherwise).
        * Saves the updated record to DynamoDB, triggering a Kinesis event via DynamoDB Streams.
      requestBody:
        required: true
        content:
          application/json:
            schema:
              $ref: "#/components/schemas/WebhookPayload"
            examples:
              payment_returned:
                summary: ACH payment returned (R02 — account closed)
                value:
                  messageId: "MSG-20250115-abc123xyz"
                  messageType: "PAYMENT_STATUS_UPDATE"
                  sendTimeStamp: "2025-01-15T20:00:05-05:00"
                  totalNbrOfTxns: 1
                  transactionData:
                    - endToEndId: "250115ABCDE1234"
                      paymentStatus: "RETURNED"
                      processingInfo:
                        statusCode: "AC04"
                        statusText: "R02 ACCOUNT CLOSED"
                      createDateTime: "2025-01-10T09:00:00-05:00"
                      crDrIndicator: "DR"
                      reqExeDate: "2025-01-10"
                      valueDate: "2025-01-12"
                      txnAmountCcy: "USD"
                      txnAmount: 9.99
                      jpmcPmtRefId: "JPMC-REF-20250115-XYZ"
                      jpmcPmtBatchId: "JPMC-BATCH-20250115-001"
              payment_rejected:
                summary: ACH payment rejected by JPM
                value:
                  messageId: "MSG-20250116-def456uvw"
                  messageType: "PAYMENT_STATUS_UPDATE"
                  sendTimeStamp: "2025-01-16T10:30:00-05:00"
                  totalNbrOfTxns: 1
                  transactionData:
                    - endToEndId: "250116FGHIJ5678"
                      paymentStatus: "REJECTED"
                      processingInfo:
                        statusCode: "AC01"
                        statusText: "Invalid account number structure"
                      createDateTime: "2025-01-16T10:00:00-05:00"
                      crDrIndicator: "DR"
                      reqExeDate: "2025-01-16"
                      valueDate: "2025-01-18"
                      txnAmountCcy: "USD"
                      txnAmount: 15.99
                      jpmcPmtRefId: "JPMC-REF-20250116-UVW"
                      jpmcPmtBatchId: "JPMC-BATCH-20250116-002"
              payment_completed:
                summary: ACH payment completed (no SQS enqueue)
                value:
                  messageId: "MSG-20250117-ghi789rst"
                  messageType: "PAYMENT_STATUS_UPDATE"
                  sendTimeStamp: "2025-01-17T08:00:00-05:00"
                  totalNbrOfTxns: 1
                  transactionData:
                    - endToEndId: "250117KLMNO9012"
                      paymentStatus: "COMPLETED"
                      processingInfo:
                        statusCode: "SUCCESS"
                        statusText: "Payment completed"
                      createDateTime: "2025-01-15T09:00:00-05:00"
                      crDrIndicator: "DR"
                      reqExeDate: "2025-01-15"
                      valueDate: "2025-01-17"
                      txnAmountCcy: "USD"
                      txnAmount: 9.99
                      jpmcPmtRefId: "JPMC-REF-20250117-RST"
                      jpmcPmtBatchId: "JPMC-BATCH-20250117-003"
      responses:
        '200':
          description: |
            Webhook received and processed successfully. If any actionable transactions
            (`RETURNED` / `REJECTED`) were present, they were enqueued to SQS. Webhooks
            containing only non-actionable statuses (e.g. `COMPLETED`) or an empty
            `transactionData` array are also acknowledged with 200.
        '400':
          description: |
            Request body could not be read (I/O error on the request body).
        '422':
          description: |
            Webhook payload failed validation. Either the body could not be parsed as JSON,
            or required fields (`messageId`, `messageType`) are missing.
        '500':
          description: |
            One or more SQS enqueue operations failed. JPM will retry the full webhook
            delivery. Duplicate SQS messages are safe because the downstream
            `jpm-webhook-processor` Lambda checks whether the payment is already in
            `FAILED` status and skips reprocessing if so — the webhook handler itself
            performs no deduplication.
