165 lines
5.4 KiB
Go
165 lines
5.4 KiB
Go
package paymenthttp
|
|
|
|
import (
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"strconv"
|
|
|
|
sresponse "git.sendico.io/tech/sxchnge/product/auth/http/v1/response"
|
|
"git.sendico.io/tech/sxchnge/product/auth/ipguard"
|
|
customer "git.sendico.io/tech/sxchnge/product/customers/model"
|
|
"git.sendico.io/tech/sxchnge/product/payments"
|
|
verification "git.sendico.io/tech/sxchnge/product/verification/store"
|
|
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
|
api "git.sendico.io/tech/sxchnge/shared/http"
|
|
"git.sendico.io/tech/sxchnge/shared/http/response"
|
|
mlogger "git.sendico.io/tech/sxchnge/shared/logging"
|
|
mservice "git.sendico.io/tech/sxchnge/shared/service"
|
|
)
|
|
|
|
type Registrar interface {
|
|
AccountHandler(mservice.Type, string, api.HTTPMethod, sresponse.AccountHandlerFunc)
|
|
}
|
|
type router struct {
|
|
service *payments.Service
|
|
logger mlogger.Logger
|
|
}
|
|
|
|
func Install(reg Registrar, s *payments.Service, logger mlogger.Logger) {
|
|
h := &router{s, logger.Named("payment_http")}
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/profile", api.Get, h.profile)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet", api.Get, h.fetch)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/quote", api.Post, h.quote)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/submit", api.Post, h.submit)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/submission", api.Post, h.reconcile)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/challenge", api.Post, h.challenge)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/resend", api.Post, h.resend)
|
|
reg.AccountHandler(mservice.Accounts, "/wallet/verify", api.Post, h.verify)
|
|
}
|
|
func decode(r *http.Request, dst any) error {
|
|
d := json.NewDecoder(io.LimitReader(r.Body, 8193))
|
|
d.DisallowUnknownFields()
|
|
if d.Decode(dst) != nil {
|
|
return payments.InvalidAmount
|
|
}
|
|
var extra any
|
|
if d.Decode(&extra) != io.EOF {
|
|
return payments.InvalidAmount
|
|
}
|
|
return nil
|
|
}
|
|
func (h *router) result(data any, err error) http.HandlerFunc {
|
|
return func(w http.ResponseWriter, r *http.Request) {
|
|
w.Header().Set("Cache-Control", "no-store")
|
|
if err == nil {
|
|
response.Ok(h.logger, data)(w, r)
|
|
return
|
|
}
|
|
code := "unavailable"
|
|
status := http.StatusServiceUnavailable
|
|
var f payments.Failure
|
|
if errors.As(err, &f) {
|
|
code = string(f)
|
|
if f != payments.Unavailable && f != payments.Uncertain {
|
|
status = http.StatusBadRequest
|
|
}
|
|
}
|
|
switch {
|
|
case errors.Is(err, verification.ErrTokenExpired):
|
|
code = "factorExpired"
|
|
status = 400
|
|
case errors.Is(err, verification.ErrTokenAttemptsExceeded):
|
|
code = "factorLocked"
|
|
status = 400
|
|
case errors.Is(err, verification.ErrTokenNotFound), errors.Is(err, verification.ErrTokenAlreadyUsed), errors.Is(err, merrors.ErrUnauthorized):
|
|
code = "factorInvalid"
|
|
status = 400
|
|
}
|
|
if throttle, ok := verification.Throttle(err); ok {
|
|
w.Header().Set("Retry-After", strconv.Itoa(max(1, int(throttle.RetryAfter.Seconds()))))
|
|
code = "factorCooldown"
|
|
status = 429
|
|
}
|
|
response.Error(h.logger, mservice.Accounts, status, code, "Unable to complete wallet operation")(w, r)
|
|
}
|
|
}
|
|
func (h *router) fetch(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
v, e := h.service.Fetch(r.Context(), a)
|
|
return h.result(fromSnapshot(v), e)
|
|
}
|
|
func (h *router) quote(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
var d requestDTO
|
|
if e := decode(r, &d); e != nil {
|
|
return h.result(nil, e)
|
|
}
|
|
v, e := h.service.Quote(r.Context(), a, d.model())
|
|
return h.result(fromQuote(v), e)
|
|
}
|
|
|
|
type actionDTO struct {
|
|
Quote string `json:"quoteId"`
|
|
Key string `json:"idempotencyKey"`
|
|
Challenge string `json:"challengeId"`
|
|
Code string `json:"code"`
|
|
}
|
|
|
|
func (h *router) submit(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
var d actionDTO
|
|
if e := decode(r, &d); e != nil {
|
|
return h.result(nil, e)
|
|
}
|
|
v, e := h.service.Submit(r.Context(), a, d.Quote, d.Key)
|
|
return h.result(fromOperation(v), e)
|
|
}
|
|
func (h *router) reconcile(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
var d actionDTO
|
|
if e := decode(r, &d); e != nil {
|
|
return h.result(nil, e)
|
|
}
|
|
v, e := h.service.Reconcile(r.Context(), a, d.Key)
|
|
if v == nil {
|
|
return h.result(nil, e)
|
|
}
|
|
return h.result(fromOperation(*v), e)
|
|
}
|
|
func requestIP(r *http.Request) string {
|
|
if ip := ipguard.ClientIP(r); ip != nil {
|
|
return ip.String()
|
|
}
|
|
return "unresolved"
|
|
}
|
|
func (h *router) challenge(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
var d actionDTO
|
|
if e := decode(r, &d); e != nil {
|
|
return h.result(nil, e)
|
|
}
|
|
v, e := h.service.Challenge(r.Context(), a, d.Quote, requestIP(r))
|
|
return h.result(fromChallenge(v), e)
|
|
}
|
|
func (h *router) resend(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
var d actionDTO
|
|
if e := decode(r, &d); e != nil {
|
|
return h.result(nil, e)
|
|
}
|
|
v, e := h.service.Resend(r.Context(), a, d.Quote, d.Challenge, d.Key, requestIP(r))
|
|
return h.result(fromChallenge(v), e)
|
|
}
|
|
func (h *router) verify(r *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
var d actionDTO
|
|
if e := decode(r, &d); e != nil {
|
|
return h.result(nil, e)
|
|
}
|
|
e := h.service.Authorize(r.Context(), a, d.Quote, d.Challenge, d.Code, d.Key)
|
|
return h.result(struct {
|
|
Key string `json:"idempotencyKey"`
|
|
}{d.Key}, e)
|
|
}
|
|
|
|
func (h *router) profile(_ *http.Request, a *customer.Account, _ *sresponse.TokenData) http.HandlerFunc {
|
|
return h.result(struct {
|
|
Locale string `json:"locale"`
|
|
}{a.Locale}, nil)
|
|
}
|