package processor import ( "context" "errors" "github.com/google/uuid" ) var ( // ErrVoucherProviderRejected is what an adapter wraps when the provider definitely // will not issue the voucher: out of stock, the product retired, the request // refused. The redemption fails and the EnakPoint go back. ErrVoucherProviderRejected = errors.New("voucher provider refused") // ErrVoucherProviderNotFound is what an adapter's Lookup wraps when the provider // has no record of the redemption: the issue request never reached it. ErrVoucherProviderNotFound = errors.New("voucher provider has no such redemption") ) // VoucherIssueRequest is one redemption of an EXTERNAL voucher sent to its provider. type VoucherIssueRequest struct { // The provider's idempotency key: asking twice for the same redemption must give // the same voucher, never a second one (docs/rfc-enakgame.md ยง7.5). RedemptionID uuid.UUID // The provider's own reference of the product, from vouchers.provider_ref. ProviderRef *string VoucherID uuid.UUID FaceValue int64 CustomerID uuid.UUID } // VoucherIssue is what a provider handed out. type VoucherIssue struct { Code string // The provider's reference of this issue, for support and reconciliation. Ref string } // VoucherProvider issues EXTERNAL vouchers (EG-801). Adapters classify every outcome // through the error they return: // // - nil: issued. // - wrapping ErrVoucherProviderRejected: definitely not issued, and never will be. // - wrapping ErrVoucherProviderNotFound (Lookup only): the provider never got it. // - anything else, a timeout above all: unknown. The redemption stays PENDING and // the recovery job asks again. type VoucherProvider interface { Issue(ctx context.Context, req VoucherIssueRequest) (*VoucherIssue, error) Lookup(ctx context.Context, req VoucherIssueRequest) (*VoucherIssue, error) } // VoucherProviders are the adapters by the name vouchers.provider holds. type VoucherProviders map[string]VoucherProvider // voucherIssueOutcome is how one call to a provider ended. type voucherIssueOutcome int const ( voucherIssued voucherIssueOutcome = iota voucherRejected voucherUnknown voucherNotFound ) func classifyVoucherIssue(issue *VoucherIssue, err error) voucherIssueOutcome { switch { case err == nil && issue != nil && issue.Code != "": return voucherIssued case errors.Is(err, ErrVoucherProviderRejected): return voucherRejected case errors.Is(err, ErrVoucherProviderNotFound): return voucherNotFound } // An error of any other kind, or a success without a code, settles nothing. return voucherUnknown }