mirror of
https://github.com/hanzo-fi/payments.git
synced 2026-10-30 20:31:20 +00:00
* fix(payments): EN-1345 reject non-positive payment-initiation amount
POST /v3/payment-initiations (and .../reverse) accepted a negative or zero
amount: the Amount (*big.Int) field was validated only as `required`, which
only guards against a nil pointer.
Add a gtZero custom validator (strictly positive big.Int) and apply it to the
create and reverse request amounts, so a non-positive amount is rejected with
400 VALIDATION instead of being recorded.
Confidence: high
Scope-risk: narrow
* fix(payments): EN-1346/EN-1347 shorten reverse-payout/transfer workflow id
Reversing a payment initiation or payout returned 500: the reverse-payout and
reverse-transfer Temporal WorkflowId embedded reversal.CreatedAt.String() (a
verbose, space-containing timestamp) in addition to the connector id and the
reversal id, pushing it past Temporal's WorkflowId length limit
("WorkflowId length exceeds limit.").
reversal.ID already uniquely identifies the reversal (reference + connector),
so drop the CreatedAt timestamp from both ids. The construction now matches the
proven-good create-payout/create-transfer ids.
Constraint: reverse ids must stay deterministic and unique per reversal
Rejected: Hash the whole workflow id | diverges from the readable id convention; removing the timestamp is enough to match the working create path
Confidence: high
Scope-risk: narrow
Not-tested: end-to-end reverse against a live Temporal server (covered via engine workflow-id unit assertions)
---------
Co-authored-by: fabrice guery <[email protected]>
83 lines
2.7 KiB
Go
83 lines
2.7 KiB
Go
package validation
|
|
|
|
import (
|
|
"fmt"
|
|
|
|
"github.com/go-playground/locales"
|
|
"github.com/go-playground/locales/en"
|
|
"github.com/go-playground/validator/v10"
|
|
|
|
ut "github.com/go-playground/universal-translator"
|
|
en_translations "github.com/go-playground/validator/v10/translations/en"
|
|
)
|
|
|
|
var (
|
|
defaultLocale locales.Translator
|
|
supportedLocales []locales.Translator
|
|
)
|
|
|
|
func init() {
|
|
defaultLocale = en.New()
|
|
supportedLocales = []locales.Translator{defaultLocale}
|
|
}
|
|
|
|
type Validator struct {
|
|
internal *validator.Validate
|
|
translator ut.Translator
|
|
}
|
|
|
|
func NewValidator() *Validator {
|
|
uni := ut.New(defaultLocale, supportedLocales...)
|
|
// TODO: non-default locale could be configured for a stack if we have non-english clients who need it
|
|
translator, _ := uni.GetTranslator(defaultLocale.Locale())
|
|
|
|
validate := validator.New(validator.WithRequiredStructEnabled())
|
|
en_translations.RegisterDefaultTranslations(validate, translator) //nolint:errcheck
|
|
|
|
registerCustomChecker("accountID", IsAccountID, "", validate, translator)
|
|
registerCustomChecker("accountType", IsAccountType, "", validate, translator)
|
|
registerCustomChecker("connectorID", IsConnectorID, "", validate, translator)
|
|
registerCustomChecker("paymentType", IsPaymentType, "", validate, translator)
|
|
registerCustomChecker("paymentScheme", IsPaymentScheme, "", validate, translator)
|
|
registerCustomChecker("paymentStatus", IsPaymentStatus, "", validate, translator)
|
|
registerCustomChecker("paymentInitiationType", IsPaymentInitiationType, "", validate, translator)
|
|
registerCustomChecker("asset", IsAsset, "", validate, translator)
|
|
registerCustomChecker("phoneNumber", IsPhoneNumber, "", validate, translator)
|
|
registerCustomChecker("email", IsEmail, "", validate, translator)
|
|
registerCustomChecker("locale", IsLocale, "", validate, translator)
|
|
registerCustomChecker("gtZero", IsPositiveBigInt, "{0} must be greater than 0", validate, translator)
|
|
return &Validator{
|
|
internal: validate,
|
|
translator: translator,
|
|
}
|
|
}
|
|
|
|
func (v *Validator) Validate(payload any) (fieldErrors validator.ValidationErrors, err error) {
|
|
rawErr := v.internal.Struct(payload)
|
|
if rawErr == nil {
|
|
return fieldErrors, nil
|
|
}
|
|
|
|
fieldErrors, ok := rawErr.(validator.ValidationErrors)
|
|
if !ok {
|
|
return fieldErrors, rawErr
|
|
}
|
|
|
|
if len(fieldErrors) > 0 {
|
|
return fieldErrors, fmt.Errorf("%s", fieldErrors[0].Translate(v.translator))
|
|
}
|
|
return fieldErrors, rawErr
|
|
}
|
|
|
|
func fieldLevelToString(fl validator.FieldLevel) (str string, err error) {
|
|
switch v := fl.Field().Interface().(type) {
|
|
case *string:
|
|
str = *fl.Field().Interface().(*string)
|
|
case string:
|
|
str = fl.Field().Interface().(string)
|
|
default:
|
|
return str, fmt.Errorf("unsupported type %T", v)
|
|
}
|
|
return str, nil
|
|
}
|