77 lines
2.6 KiB
Go
77 lines
2.6 KiB
Go
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
|
||
|
|
}
|