package sendico import ( "bytes" "context" "encoding/json" "io" "net/http" "time" "git.sendico.io/tech/sxchnge/product/payments" merrors "git.sendico.io/tech/sxchnge/shared/errors" ) type moneyDTO struct { Amount string `json:"amount"` Currency string `json:"currency"` } type endpointDTO struct { Type string `json:"type"` Data map[string]string `json:"data"` } // Explicit wire fields avoid coupling local domain models to provider JSON. type quoteRequestDTO struct { Key string `json:"idempotencyKey"` Intent paymentIntentDTO `json:"intent"` } type paymentIntentDTO struct { Kind string `json:"kind"` Source endpointDTO `json:"source"` Destination endpointDTO `json:"destination"` Amount moneyDTO `json:"amount"` Subject string `json:"external_subject_ref"` FeeTreatment string `json:"fee_treatment"` } type quoteAmountsDTO struct { Principal moneyDTO `json:"sourcePrincipal"` Debit moneyDTO `json:"sourceDebitTotal"` Credit moneyDTO `json:"destinationSettlement"` } type paymentQuoteDTO struct { ID string `json:"quoteRef"` State string `json:"state"` Executable bool `json:"executable"` BlockReason string `json:"blockReason"` Expires time.Time `json:"expiresAt"` Amounts quoteAmountsDTO `json:"amounts"` } type paymentDTO struct { ID string `json:"paymentRef"` State string `json:"state"` Quote paymentQuoteDTO `json:"lastQuote"` Created time.Time `json:"createdAt"` } type accountDTO struct { ID string `json:"ledgerAccountRef"` ScopeRef string `json:"organizationRef"` Owner string `json:"ownerRef"` Currency string `json:"currency"` Type string `json:"accountType"` LedgerRole string `json:"role"` Status string `json:"status"` Metadata map[string]string `json:"metadata"` } type currencyDTO struct { Code string `json:"code"` Scale *int `json:"accountingScale"` } type balanceDTO struct { ID string `json:"ledgerAccountRef"` Money moneyDTO `json:"balance"` } func (c *Client) paymentRequest(ctx context.Context, method, path string, payload, dst any) error { token, err := c.token() if err != nil { return err } var body []byte if payload != nil { body, err = json.Marshal(payload) if err != nil { return merrors.OpaqueWrap(payments.Unavailable, err, "encode Sendico payment request") } } req, err := http.NewRequestWithContext(ctx, method, c.base+path, bytes.NewReader(body)) if err != nil { return merrors.OpaqueWrap(payments.Unavailable, err, "create Sendico payment request") } req.Header.Set("Authorization", "Bearer "+token) req.Header.Set("Content-Type", "application/json") client := *c.http client.Timeout = 30 * time.Second res, err := client.Do(req) if err != nil { return merrors.OpaqueWrap(payments.Unavailable, err, "Sendico payment request failed") } defer func() { _ = res.Body.Close() }() // Never forward upstream errors, credentials, or arbitrary error bodies. if res.StatusCode != http.StatusOK { return payments.Unavailable } var envelope struct { Status string `json:"status"` Data json.RawMessage `json:"data"` } d := json.NewDecoder(io.LimitReader(res.Body, 4*1024*1024+1)) if err = d.Decode(&envelope); err != nil { return merrors.OpaqueWrap(payments.Unavailable, err, "decode Sendico payment envelope") } if envelope.Status != "success" { return payments.Unavailable } var extra any if d.Decode(&extra) != io.EOF { return payments.Unavailable } if err = json.Unmarshal(envelope.Data, dst); err != nil { return merrors.OpaqueWrap(payments.Unavailable, err, "decode Sendico payment payload") } return nil }