# ASF Fundraiser External Source Experience API
#
# Source of truth for this spec lives in this file. The original Anypoint
# export is preserved at spec/source/anypoint-export.json (Swagger 2.0). This
# file is a hand-authored OpenAPI 3.1 rewrite — descriptions, schemas, error
# documentation and security model have been edited for partner consumption,
# not generated mechanically.
#
# Lint with: npx @redocly/cli@latest lint spec/openapi.yaml

openapi: 3.1.0

info:
  title: ASF Fundraiser External Source Experience API
  summary: Integrate external donation, mandate and remittance flows with the Australian Sports Foundation.
  description: |
    The Fundraiser External Source Experience API lets approved partner platforms
    (for example the ASF Shopify App and revolutioniseSPORT integration) push
    donations, set up direct-debit mandates and reconcile remittances against
    fundraisers registered with the **Australian Sports Foundation (ASF)**.

    All endpoints accept and return `application/json`. Every request must carry
    HTTP Basic credentials *plus* the matching `client_id` and `client_secret`
    headers; see the **Authentication** section. Every request must also include
    an `X-Correlation-ID` header so that the request can be traced through ASF's
    integration platform.

    ## Conventions

    - Successful responses use the `SuccessEnvelope` shape (`apiVersion`,
      `correlationId`, `method`, `data.items`).
    - Error responses use the `ErrorEnvelope` shape (`apiVersion`,
      `correlationId`, `method`, `error.{code,timeStamp,message,cause}`).
    - Timestamps are ISO 8601.
    - Monetary `amount` fields are decimal strings or numbers in AUD unless
      noted otherwise.

    ## Error codes

    | Code | When it fires |
    | --- | --- |
    | `FUNDRAISER:VALIDATION` | The fundraiser referenced by `fundraiserId` is not enrolled with the channel making the request, or required fields failed validation. Returned with `400`. |
    | `FUNDRAISER:UNKNOWN` | Unhandled exception inside ASF's integration platform. Returned with `500`. Retry with backoff; if it persists contact `integrations@asf.org.au`. |
    | `FUNDRAISER:REMITTANCE POST` | A remittance batch could not be accepted because at least one referenced fundraiser is not enrolled with ASF. Returned with `406`. |
    | `ACCOUNT:NOT_FOUND` | No fundraiser account exists for the supplied id. Returned with `404`. |
  version: 1.0.0
  contact:
    name: ASF integrations team
    email: integrations@asf.org.au
  # No `license` block: this is a proprietary integration API restricted to
  # approved ASF partners. Credentials are issued out-of-band; there is no
  # public licence page or SPDX identifier to reference.

servers:
  - url: https://api.asf.org.au/fundraisers/api/v1
    description: Production
  - url: https://sit.api.asf.org.au/fundraisers/api/v1
    description: Pre-production (partner UAT)

tags:
  - name: Fundraisers
    description: Validate and look up fundraiser enrolments before posting donations or mandates.
  - name: Donations
    description: Post donations made on a partner platform back to ASF.
  - name: Mandates
    description: Create and inspect direct-debit mandates for recurring donations.
  - name: Remittances
    description: Reconcile bank-settled remittances against donations already created in ASF.
  - name: Accounts
    description: Read-only fundraiser account details, including products and campaigns.
  - name: Operational
    description: Liveness, health, and version probes. Useful during partner onboarding and on-call.

security:
  - basicAuth: []
    clientId: []
    clientSecret: []

paths:

  /health:
    get:
      tags: [Operational]
      operationId: getHealth
      summary: Service health
      description: Returns a structured health document for the API. Suitable for synthetic monitoring.
      responses:
        "200":
          description: API is healthy.
          content:
            application/json:
              examples:
                healthy:
                  summary: Service up
                  value:
                    serviceHealth:
                      name: eway-sapi-v1
                      version: 1.0.0
                      serviceType: http
                      status: UP
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /ping:
    get:
      tags: [Operational]
      operationId: getPing
      summary: Liveness probe
      description: Lightweight liveness check. Returns `{"status":"Ok"}` when the API is reachable.
      responses:
        "200":
          description: API is reachable.
          content:
            application/json:
              examples:
                ok:
                  value:
                    status: Ok
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /info:
    get:
      tags: [Operational]
      operationId: getInfo
      summary: API metadata
      description: Returns the running application's name, description, and semantic version.
      responses:
        "200":
          description: API metadata.
          content:
            application/json:
              examples:
                info:
                  value:
                    applicationInfo:
                      name: eway-sapi-v1
                      description: This API syncs data to eway.
                      version: 1.0.0
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /fundraisers:
    get:
      tags: [Fundraisers]
      operationId: validateFundraiser
      summary: Validate a fundraiser enrolment
      description: |
        Confirms that the fundraiser identified by `id` has an active enrolment
        with the supplied `channel`. Call this before posting a donation or
        creating a mandate so the partner UI can fail fast with a clear error.
      parameters:
        - $ref: "#/components/parameters/CorrelationId"
        - in: query
          name: channel
          required: true
          schema: { $ref: "#/components/schemas/SourceChannel" }
          description: Partner channel making the request.
        - in: query
          name: id
          required: true
          schema: { type: string }
          description: Salesforce fundraiser ID (typically begins with `001`).
      responses:
        "200":
          description: Fundraiser is enrolled with the channel.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items: { $ref: "#/components/schemas/FundraiserSummary" }
              examples:
                validated:
                  summary: Fundraiser validated
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /fundraisers
                    data:
                      items:
                        name: Australian Football League
                        fundraiserId: "001Bn00000IEwbFIAT"
                        productEnrollmentId: "a0T0T000006MMN2UAO"
                        status: Enrollment Validated
        "400":
          description: Validation failed (`FUNDRAISER:VALIDATION`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              examples:
                notEnrolled:
                  summary: Fundraiser not enrolled with the channel
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /fundraisers
                    error:
                      code: "FUNDRAISER:VALIDATION"
                      timeStamp: "2024-05-14T22:59:08"
                      message: Fundraiser Validation Unsuccessful
                      cause: "Fundraiser with Id:001Bn00000IEwbFIAT doesn't have an active enrollment with Shopify App Product"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /donations:
    post:
      tags: [Donations]
      operationId: createDonation
      summary: Record a donation
      description: |
        Records a donation that has already been collected on the partner
        platform against the fundraiser identified by `source.fundraiserId`.
        ASF will create an internal donation record and return its
        `donationReferenceId`, which partners should persist for later
        remittance reconciliation.
      parameters:
        - $ref: "#/components/parameters/CorrelationId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/DonationRequest" }
            examples:
              shopify:
                summary: Shopify App donation
                value:
                  source:
                    channel: Shopify App
                    fundraiserId: "001Bn00000IEwbFIAT"
                  donor:
                    email: john@example.com
                    firstName: John
                    lastName: Smith
                    mobile: "0410123123"
                    businessName: Broke Corporation
                    billingAddress:
                      street: 123 Shipping Street
                      city: Shippington
                      state: ACT
                      postalCode: "40003"
                      country: AU
                    verifiedEmail: john@example.com
                  transaction:
                    id: "504789900"
                    date: "2024-03-13T16:09:54-04:00"
                    amount: "250"
                  sourceMetadata:
                    orderNumber: "450789469"
                    orderTotal: "250"
                    customerId: CUST-1234
                    orderDate: "2024-01-13T16:09:54-04:00"
              revsport:
                summary: revolutioniseSPORT donation
                value:
                  source:
                    channel: revolutioniseSPORT Integration
                    fundraiserId: "001Bn00000IEwbFIAT"
                  donor:
                    email: john@example.com
                    firstName: John
                    lastName: Smith
                    mobile: "0410123123"
                    businessName: Broke Corporation
                    billingAddress:
                      street: 123 Shipping Street
                      city: Shippington
                      state: NSW
                      postalCode: "40003"
                      country: Australia
                    verifiedEmail: john@example.com
                  transaction:
                    id: "504789900"
                    date: "2024-03-13T16:09:54-04:00"
                    amount: "250"
                  sourceMetadata:
                    registrationId: 954uOVtyJz7K
                    externalId: hgewjherhi
      responses:
        "200":
          description: Donation recorded.
          content:
            application/json:
              schema:
                allOf:
                  - $ref: "#/components/schemas/SuccessEnvelope"
                  - type: object
                    properties:
                      data:
                        type: object
                        properties:
                          items:
                            type: object
                            properties:
                              donationReferenceId:
                                type: string
                                description: ASF-issued reference for the recorded donation.
              examples:
                created:
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /donations
                    data:
                      items:
                        donationReferenceId: 0000468-000788
        "400":
          description: Validation failed (`FUNDRAISER:VALIDATION`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /mandates:
    get:
      tags: [Mandates]
      operationId: getMandate
      summary: Retrieve a mandate
      description: |
        Returns the current state of a previously-created mandate, including
        the hosted mandate confirmation URL once it is available. The URL
        appears once the mandate transitions to a `succeeded`-style status;
        before then `mandate` will be an empty object.
      parameters:
        - $ref: "#/components/parameters/CorrelationId"
        - in: query
          name: id
          required: true
          schema: { type: string }
          description: Mandate identifier returned from `POST /mandates`.
      responses:
        "200":
          description: Current mandate state.
          content:
            application/json:
              examples:
                ready:
                  summary: Mandate URL available
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /mandates
                    data:
                      items:
                        mandateId: mandate_1PQglAB1RdRGi0F3c1q4OzKr
                        status: succeeded
                        mandate:
                          acceptedAt: "2024-06-12T02:17:59Z"
                          paymentMethod: au_becs_debit
                          url: https://payments.stripe.com/au_becs_debit/mandate/EXAMPLE
                pending:
                  summary: Mandate not yet completed
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /mandates
                    data:
                      items:
                        mandateId: mandate_1PQglAB1RdRGi0F3c1q4OzKr
                        status: requires_payment_method
                        mandate: {}
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

    post:
      tags: [Mandates]
      operationId: createMandate
      summary: Create a payment mandate
      description: |
        Creates a payment-method mandate against the supplied fundraiser so
        that future scheduled donations can be debited automatically. Currently
        only `Direct Debit` mandates are supported. The response includes a
        `clientSecret` that the partner front-end can use to confirm the
        mandate via Stripe.js.
      parameters:
        - $ref: "#/components/parameters/CorrelationId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/MandateRequest" }
            examples:
              shopify:
                summary: Direct Debit mandate
                value:
                  source:
                    channel: Shopify App
                    fundraiserId: "0019r00000ZvPaDAAV"
                    fundraiserAccountName: Muffin Fundraiser
                    productEnrollmentId: "a0NBn00000305NmMAI"
                  fundraiser:
                    firstName: Jenny
                    lastName: Rosen
                    email: jennyrosen@example.com
                  paymentMethod: Direct Debit
      responses:
        "200":
          description: Mandate created.
          content:
            application/json:
              examples:
                accepted:
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /mandates
                    data:
                      items:
                        fundraiserId: "001Bn00000IEwbFIAT"
                        clientSecret: seti_EXAMPLE_secret_EXAMPLE
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /remittances:
    post:
      tags: [Remittances]
      operationId: createRemittances
      summary: Post a remittance batch
      description: |
        Reconciles a batch of bank-settled remittances against donations
        previously created in ASF. Each remittance entry references a bank
        transaction identifier and the donation references it covers.
        Accepted requests return `202 Accepted` and are processed
        asynchronously; downstream errors are reported by ASF out-of-band.
      parameters:
        - $ref: "#/components/parameters/CorrelationId"
      requestBody:
        required: true
        content:
          application/json:
            schema: { $ref: "#/components/schemas/RemittancesRequest" }
            examples:
              standard:
                value:
                  source:
                    channel: revolutioniseSPORT Integration
                    fundraiserId: "001Bn00000IEwbFIAT"
                  remittances:
                    - bankReferenceId: "123456789"
                      amount: 1500.00
                      donationIds:
                        - 0000468-000788
                        - 0000468-000789
                        - 0000468-000790
                    - bankReferenceId: "1222222"
                      amount: 500.00
                      donationIds:
                        - 0000468-000791
      responses:
        "202":
          description: Remittance batch accepted for asynchronous processing.
          content:
            application/json:
              examples:
                accepted:
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /remittance
                    data:
                      items:
                        status: Request Accepted
        "406":
          description: Remittance not accepted (`FUNDRAISER:REMITTANCE POST`). At least one referenced fundraiser is not enrolled with ASF.
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              examples:
                notEnrolled:
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /remittance
                    error:
                      code: "FUNDRAISER:REMITTANCE POST"
                      timeStamp: "2024-05-17T19:10:31.158+10:00"
                      message: Fundraiser remittance processing request not accepted
                      cause: "Fundraiser with Id:0010T00000juGMKQA2 doesn't have an active enrollment with ASF system"
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

  /accounts/{id}:
    get:
      tags: [Accounts]
      operationId: getAccount
      summary: Get fundraiser account details
      description: |
        Returns descriptive details for a fundraiser account, including the
        active products and their campaigns for the supplied `channel`.
        Useful for hydrating partner-side product-discovery UIs.
      parameters:
        - $ref: "#/components/parameters/CorrelationId"
        - in: path
          name: id
          required: true
          schema: { type: string }
          description: Salesforce fundraiser account ID.
        - in: query
          name: channel
          required: true
          schema: { $ref: "#/components/schemas/SourceChannel" }
          description: Filter products to those activated for this channel.
      responses:
        "200":
          description: Account details.
          content:
            application/json:
              examples:
                found:
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /accounts/:id
                    data:
                      items:
                        name: Australian Football League (AFL)
                        memberNumber: "000028"
                        products:
                          - name: revolutioniseSPORT Integration
                            description: We strive to be consistently competitive in ACT hockey…
                            status: Activated
                            logo: https://content.example/logo.png
                            coverImage: https://content.example/cover.png
                            donationAmounts: [12, 23, 50, 100]
                            story: We strive to be consistently competitive…
                            campaigns: {}
        "404":
          description: Account not found (`ACCOUNT:NOT_FOUND`).
          content:
            application/json:
              schema: { $ref: "#/components/schemas/ErrorEnvelope" }
              examples:
                notFound:
                  value:
                    apiVersion: "1.0.0"
                    correlationId: "210c68180-2ac6-11ee-b000-111"
                    method: /accounts/:id
                    error:
                      code: "ACCOUNT:NOT_FOUND"
                      timeStamp: "2024-09-18T12:00:00Z"
                      message: Account with ID not found.
                      cause: The account ID provided does not exist in the system.
        "401": { $ref: "#/components/responses/Unauthorized" }
        "429": { $ref: "#/components/responses/QuotaExceeded" }
        "500": { $ref: "#/components/responses/ServerError" }
        "503": { $ref: "#/components/responses/ServiceUnavailable" }

components:

  securitySchemes:
    basicAuth:
      type: http
      scheme: basic
      description: |
        HTTP Basic credentials issued to the integration partner. Combine with
        `clientId` and `clientSecret` headers — all three are required on every
        secured request.
    clientId:
      type: apiKey
      in: header
      name: client_id
      description: Partner client identifier issued by the ASF integrations team.
    clientSecret:
      type: apiKey
      in: header
      name: client_secret
      description: Partner client secret issued by the ASF integrations team. Treat as confidential.

  parameters:
    CorrelationId:
      in: header
      name: X-Correlation-ID
      required: true
      schema:
        type: string
        format: uuid
      description: |
        Identifier used to trace this request across ASF's integration platform
        and downstream systems. Generate a fresh UUID per outbound request and
        log it alongside any error response.

  schemas:

    SourceChannel:
      type: string
      description: Partner channel making the request.
      enum:
        - Shopify App
        - revolutioniseSPORT Integration

    Source:
      type: object
      required: [channel]
      properties:
        channel: { $ref: "#/components/schemas/SourceChannel" }
        fundraiserId:
          type: string
          description: Salesforce fundraiser identifier.
        fundraiserAccountName:
          type: string
          description: Display name of the fundraiser's account, where known.
        productEnrollmentId:
          type: string
          description: Identifier for the product enrolment associated with this fundraiser.

    BillingAddress:
      type: object
      required: [street, city, state, postalCode, country]
      properties:
        street: { type: string }
        city: { type: string }
        state:
          type: string
          description: Australian state or territory (`ACT`, `NSW`, `NT`, `QLD`, `SA`, `TAS`, `VIC`, `WA`) — full name is also accepted.
        postalCode: { type: string }
        country:
          type: string
          description: ISO-3166 alpha-2 code (`AU`) or full country name (`Australia`).

    Donor:
      type: object
      required: [email, firstName, lastName, mobile, billingAddress]
      properties:
        email: { type: string, format: email }
        firstName: { type: string }
        lastName: { type: string }
        mobile:
          type: string
          description: Mobile phone number, E.164 preferred but local Australian format accepted.
        businessName:
          type: string
          description: Donor's business name, if the donation is on behalf of a company.
        billingAddress: { $ref: "#/components/schemas/BillingAddress" }
        verifiedEmail:
          type: string
          format: email
          description: Email address verified by the partner platform, if different from `email`.

    Transaction:
      type: object
      properties:
        id:
          type: string
          description: Partner-side transaction identifier.
        date:
          type: string
          format: date-time
          description: ISO 8601 transaction timestamp.
        amount:
          type: string
          description: Transaction amount as a decimal string, in AUD.

    DonationRequest:
      type: object
      required: [source, donor, transaction]
      description: Payload for `POST /donations`.
      properties:
        source: { $ref: "#/components/schemas/Source" }
        donor: { $ref: "#/components/schemas/Donor" }
        transaction: { $ref: "#/components/schemas/Transaction" }
        sourceMetadata:
          type: object
          description: Free-form metadata describing the partner-side context for this donation. Stored verbatim and surfaced back via reporting.
          additionalProperties: true

    MandateRequest:
      type: object
      required: [source, fundraiser, paymentMethod]
      description: Payload for `POST /mandates`.
      properties:
        source: { $ref: "#/components/schemas/Source" }
        fundraiser:
          type: object
          required: [firstName, lastName, email]
          properties:
            firstName: { type: string }
            lastName: { type: string }
            email: { type: string, format: email }
        paymentMethod:
          type: string
          description: Payment method for the mandate. Currently only `Direct Debit` is supported.
          enum: [Direct Debit]

    Remittance:
      type: object
      required: [bankReferenceId, amount, donationIds]
      properties:
        bankReferenceId:
          type: string
          description: Bank-issued reference for the settled transaction.
        amount:
          type: number
          description: Settled amount in AUD.
        donationIds:
          type: array
          description: Donation reference ids covered by this remittance entry.
          items: { type: string }

    RemittancesRequest:
      type: object
      required: [source, remittances]
      description: Payload for `POST /remittances`.
      properties:
        source: { $ref: "#/components/schemas/Source" }
        remittances:
          type: array
          items: { $ref: "#/components/schemas/Remittance" }

    FundraiserSummary:
      type: object
      properties:
        name: { type: string }
        fundraiserId: { type: string }
        productEnrollmentId: { type: string }
        status:
          type: string
          description: Human-readable enrolment status, e.g. `Enrollment Validated`.

    SuccessEnvelope:
      type: object
      required: [apiVersion, correlationId, method, data]
      properties:
        apiVersion:
          type: string
          description: API version that produced this response.
        correlationId:
          type: string
          description: Echoes the `X-Correlation-ID` from the request.
        method:
          type: string
          description: Endpoint that produced this response.
        data:
          type: object
          properties:
            items:
              description: Endpoint-specific success payload.

    ErrorEnvelope:
      type: object
      required: [apiVersion, correlationId, method, error]
      properties:
        apiVersion: { type: string }
        correlationId: { type: string }
        method: { type: string }
        error:
          type: object
          required: [code, timeStamp, message]
          properties:
            code:
              type: string
              description: ASF error code. See the **Error codes** table in the introduction.
            timeStamp:
              type: string
              description: ISO 8601 timestamp when the error was generated.
            message:
              type: string
              description: Human-readable error summary.
            cause:
              type: string
              description: Optional, more-specific explanation of the failure.

  responses:

    Unauthorized:
      description: The supplied `client_id`, `client_secret` or HTTP Basic credentials are invalid or do not have access to this operation.
    QuotaExceeded:
      description: The client has exhausted its request quota for the current period.
    ServerError:
      description: Unhandled server error (`FUNDRAISER:UNKNOWN`).
      content:
        application/json:
          schema: { $ref: "#/components/schemas/ErrorEnvelope" }
          examples:
            unknown:
              value:
                apiVersion: "1.0.0"
                correlationId: "76e09919-c78e-4e2b-b862-d2b87472ba96"
                method: /
                error:
                  code: "FUNDRAISER:UNKNOWN"
                  timeStamp: "2024-06-04T03:21:18"
                  message: Unhandled Exception Occurred
                  cause: Unable to validate Fundraiser, due to unexpected exception.
    ServiceUnavailable:
      description: The server is temporarily unable to handle the request. Retry with backoff.
