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) }