Bootstrap copied BFF and local accounts with public Sendico contracts (#34)
This commit is contained in:
1 parent
67dc5b5b1e
commit
7062672a87
556 files changed
+42291
-777
No files matched your search
@@ -0,0 +1,25 @@
|
||||
package merrors
|
||||
|
||||
var ErrAccountingInvalidPosting = Define("accounting.invalid_posting")
|
||||
|
||||
var ErrAccountingUnbalancedPosting = Define("accounting.unbalanced_posting")
|
||||
|
||||
var ErrAccountingAssetMismatch = Define("accounting.asset_mismatch")
|
||||
|
||||
var ErrAccountingAccountNotFound = Define("accounting.account_not_found")
|
||||
|
||||
var ErrAccountingAccountInactive = Define("accounting.account_inactive")
|
||||
|
||||
var ErrAccountingInvalidAmount = Define("accounting.invalid_amount")
|
||||
|
||||
var ErrAccountingInvalidIdempotency = Define("accounting.invalid_idempotency")
|
||||
|
||||
var ErrAccountingIdempotencyConflict = Define("accounting.idempotency_conflict")
|
||||
|
||||
var ErrAccountingStorageFailed = Define("accounting.storage_failed")
|
||||
|
||||
var ErrAccountingDuplicateKey = Define("accounting.duplicate_key")
|
||||
|
||||
var ErrAccountingOptimisticConflict = Define("accounting.optimistic_conflict")
|
||||
|
||||
var ErrAccountingInvalidStorageState = Define("accounting.invalid_storage_state")
|
||||
@@ -0,0 +1,20 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
const ReasonAMLSourceIdempotencyConflict = "AML_SOURCE_IDEMPOTENCY_CONFLICT"
|
||||
|
||||
var ErrAMLSourceIdempotencyConflict = Define("aml.source_idempotency_conflict")
|
||||
|
||||
func AMLSourceIdempotencyConflict(msg string) error {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg == "" {
|
||||
msg = ErrAMLSourceIdempotencyConflict.Error()
|
||||
}
|
||||
return classifiedError{
|
||||
base: ErrDataConflict,
|
||||
code: ErrAMLSourceIdempotencyConflict,
|
||||
msg: msg,
|
||||
reason: ReasonAMLSourceIdempotencyConflict,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestAMLSourceIdempotencyConflictReasonRoundTrip(t *testing.T) {
|
||||
err := AMLSourceIdempotencyConflict("key is bound to another source mutation")
|
||||
if !errors.Is(err, ErrDataConflict) || !errors.Is(err, ErrAMLSourceIdempotencyConflict) {
|
||||
t.Fatalf("AML source idempotency conflict classification mismatch: %v", err)
|
||||
}
|
||||
reason, ok := Reason(err)
|
||||
if !ok || reason != ReasonAMLSourceIdempotencyConflict {
|
||||
t.Fatalf("AML source idempotency reason mismatch: reason=%q ok=%t", reason, ok)
|
||||
}
|
||||
restored, ok := FromReason(reason, err.Error())
|
||||
if !ok || !errors.Is(restored, ErrAMLSourceIdempotencyConflict) {
|
||||
t.Fatalf("AML source idempotency reason did not round trip: %v", restored)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ReasonChainExecutionSigningUnavailable = "chain_execution.signing_unavailable"
|
||||
ReasonChainExecutionUnsupportedCapability = "chain_execution.unsupported_capability"
|
||||
ReasonChainExecutionInsufficientBalance = "chain_execution.insufficient_balance"
|
||||
ReasonChainExecutionInsufficientNativeResources = "chain_execution.insufficient_native_resources"
|
||||
ReasonChainExecutionDestinationIneligible = "chain_execution.destination_ineligible"
|
||||
)
|
||||
|
||||
// ChainExecutionPrecondition creates a typed, reason-bearing permanent
|
||||
// preparation failure. Callers can classify it with both errors.Is and Reason.
|
||||
func ChainExecutionPrecondition(reason, message string) error {
|
||||
return classifiedError{
|
||||
base: ErrFailedPrecondition,
|
||||
msg: strings.TrimSpace(message),
|
||||
reason: strings.TrimSpace(reason),
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestChainExecutionPreconditionCarriesClassAndReason(t *testing.T) {
|
||||
err := ChainExecutionPrecondition(ReasonChainExecutionInsufficientBalance, "source balance is insufficient")
|
||||
if !errors.Is(err, ErrFailedPrecondition) {
|
||||
t.Fatal("expected failed-precondition classification")
|
||||
}
|
||||
reason, ok := Reason(err)
|
||||
if !ok || reason != ReasonChainExecutionInsufficientBalance {
|
||||
t.Fatalf("unexpected reason: %q", reason)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
type classifiedError struct {
|
||||
base error
|
||||
code error
|
||||
msg string
|
||||
reason string
|
||||
publicMessage bool
|
||||
argumentNames []string
|
||||
}
|
||||
|
||||
func (e classifiedError) Error() string {
|
||||
if e.publicMessage {
|
||||
msg := strings.TrimSpace(e.msg)
|
||||
if msg != "" {
|
||||
return msg
|
||||
}
|
||||
if e.code != nil {
|
||||
return e.code.Error()
|
||||
}
|
||||
if e.base != nil {
|
||||
return e.base.Error()
|
||||
}
|
||||
return ""
|
||||
}
|
||||
return Wrap(e.base, e.msg).Error()
|
||||
}
|
||||
|
||||
func (e classifiedError) Unwrap() []error {
|
||||
causes := make([]error, 0, 2)
|
||||
if e.base != nil {
|
||||
causes = append(causes, e.base)
|
||||
}
|
||||
if e.code != nil {
|
||||
causes = append(causes, e.code)
|
||||
}
|
||||
return causes
|
||||
}
|
||||
|
||||
func (e classifiedError) Reason() string {
|
||||
return strings.TrimSpace(e.reason)
|
||||
}
|
||||
|
||||
func (e classifiedError) InvalidArgumentFields() []string {
|
||||
return append([]string(nil), e.argumentNames...)
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ReasonDocumentsReportOwned = "DOCUMENTS_REPORT_OWNED"
|
||||
ReasonDocumentsQuotationExpired = "DOCUMENTS_QUOTATION_EXPIRED"
|
||||
)
|
||||
|
||||
var ErrDocumentsReportOwned = Define("documents.report_owned")
|
||||
|
||||
var ErrDocumentsQuotationExpired = Define("documents.quotation_expired")
|
||||
|
||||
// DocumentsReportOwned reports that an active or settled payout owns a
|
||||
// report, so callers must not mutate its document lifecycle.
|
||||
func DocumentsReportOwned(msg string) error {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg == "" {
|
||||
msg = "report is owned by a payout"
|
||||
}
|
||||
return classifiedError{
|
||||
base: ErrDataConflict,
|
||||
code: ErrDocumentsReportOwned,
|
||||
msg: msg,
|
||||
reason: ReasonDocumentsReportOwned,
|
||||
publicMessage: true,
|
||||
}
|
||||
}
|
||||
|
||||
// DocumentsQuotationExpired reports that a payout registry still references
|
||||
// a quotation which can no longer be executed. A fresh quotation is required.
|
||||
func DocumentsQuotationExpired(msg string) error {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg == "" {
|
||||
msg = "the payout quotation has expired; request a new quotation"
|
||||
}
|
||||
return classifiedError{
|
||||
base: ErrFailedPrecondition,
|
||||
code: ErrDocumentsQuotationExpired,
|
||||
msg: msg,
|
||||
reason: ReasonDocumentsQuotationExpired,
|
||||
publicMessage: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestDocumentsReportOwnedReasonRoundTrip(t *testing.T) {
|
||||
err := DocumentsReportOwned("report has an active payout")
|
||||
if !errors.Is(err, ErrDataConflict) || !errors.Is(err, ErrDocumentsReportOwned) {
|
||||
t.Fatalf("DocumentsReportOwned() classification = %v", err)
|
||||
}
|
||||
reason, ok := Reason(err)
|
||||
if !ok || reason != ReasonDocumentsReportOwned {
|
||||
t.Fatalf("Reason() = %q, %t", reason, ok)
|
||||
}
|
||||
restored, ok := FromReason(reason, "report has an active payout")
|
||||
if !ok || !errors.Is(restored, ErrDataConflict) || !errors.Is(restored, ErrDocumentsReportOwned) {
|
||||
t.Fatalf("FromReason() = %v, %t", restored, ok)
|
||||
}
|
||||
}
|
||||
|
||||
func TestDocumentsQuotationExpiredReasonRoundTrip(t *testing.T) {
|
||||
err := DocumentsQuotationExpired("quotation expired")
|
||||
if !errors.Is(err, ErrFailedPrecondition) || !errors.Is(err, ErrDocumentsQuotationExpired) {
|
||||
t.Fatalf("DocumentsQuotationExpired() classification = %v", err)
|
||||
}
|
||||
reason, ok := Reason(err)
|
||||
if !ok || reason != ReasonDocumentsQuotationExpired {
|
||||
t.Fatalf("Reason() = %q, %t", reason, ok)
|
||||
}
|
||||
restored, ok := FromReason(reason, "quotation expired")
|
||||
if !ok || !errors.Is(restored, ErrFailedPrecondition) ||
|
||||
!errors.Is(restored, ErrDocumentsQuotationExpired) {
|
||||
t.Fatalf("FromReason() = %v, %t", restored, ok)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,268 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
type definedError string
|
||||
|
||||
func (e definedError) Error() string {
|
||||
return string(e)
|
||||
}
|
||||
|
||||
type wrappedError struct {
|
||||
base error
|
||||
msg string
|
||||
}
|
||||
|
||||
// opaqueWrappedError preserves error classification and causal inspection
|
||||
// without including the cause text in Error. Use it at boundaries where a
|
||||
// storage or provider error can contain secrets or sensitive identifiers.
|
||||
type opaqueWrappedError struct {
|
||||
base error
|
||||
cause error
|
||||
msg string
|
||||
}
|
||||
|
||||
func (e wrappedError) Error() string {
|
||||
baseText := ""
|
||||
if e.base != nil {
|
||||
baseText = strings.TrimSpace(e.base.Error())
|
||||
}
|
||||
msg := strings.TrimSpace(e.msg)
|
||||
|
||||
switch {
|
||||
case baseText == "":
|
||||
return msg
|
||||
case msg == "":
|
||||
return baseText
|
||||
default:
|
||||
return baseText + ": " + msg
|
||||
}
|
||||
}
|
||||
|
||||
func (e wrappedError) Unwrap() error {
|
||||
return e.base
|
||||
}
|
||||
|
||||
func (e opaqueWrappedError) Error() string {
|
||||
return Wrap(e.base, e.msg).Error()
|
||||
}
|
||||
|
||||
func (e opaqueWrappedError) Unwrap() []error {
|
||||
causes := make([]error, 0, 2)
|
||||
if e.base != nil {
|
||||
causes = append(causes, e.base)
|
||||
}
|
||||
if e.cause != nil {
|
||||
causes = append(causes, e.cause)
|
||||
}
|
||||
return causes
|
||||
}
|
||||
|
||||
func Define(msg string) error {
|
||||
return definedError(msg)
|
||||
}
|
||||
|
||||
func Message(msg string) error {
|
||||
return definedError(msg)
|
||||
}
|
||||
|
||||
func Messagef(format string, args ...any) error {
|
||||
return Message(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func Wrap(err error, msg string) error {
|
||||
return wrappedError{
|
||||
base: err,
|
||||
msg: msg,
|
||||
}
|
||||
}
|
||||
|
||||
func Wrapf(err error, format string, args ...any) error {
|
||||
return Wrap(err, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// OpaqueWrap adds safe context while retaining the cause for errors.Is and
|
||||
// errors.As. Unlike Wrap, it deliberately excludes the cause text from Error.
|
||||
func OpaqueWrap(base, cause error, msg string) error {
|
||||
return opaqueWrappedError{base: base, cause: cause, msg: msg}
|
||||
}
|
||||
|
||||
var ErrNotImplemented = Define("notImplemented")
|
||||
|
||||
func NotImplemented(msg string) error {
|
||||
return Wrap(ErrNotImplemented, msg)
|
||||
}
|
||||
|
||||
var ErrNoData = Define("noData")
|
||||
|
||||
func NoData(msg string) error {
|
||||
return Wrap(ErrNoData, msg)
|
||||
}
|
||||
|
||||
func NoDataf(format string, args ...any) error {
|
||||
return NoData(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
var ErrInternal = Define("internal")
|
||||
|
||||
func Internal(msg string) error {
|
||||
return Wrap(ErrInternal, msg)
|
||||
}
|
||||
|
||||
func Internalf(format string, args ...any) error {
|
||||
return Internal(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
var ErrInvalidArg = Define("invalidArgError")
|
||||
|
||||
func InvalidArgument(msg string, argumentNames ...string) error {
|
||||
return Wrap(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...))
|
||||
}
|
||||
|
||||
func InvalidArgumentf(format string, args ...any) error {
|
||||
return InvalidArgument(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func InvalidArgumentWithNamesf(argumentNames []string, format string, args ...any) error {
|
||||
return InvalidArgument(fmt.Sprintf(format, args...), argumentNames...)
|
||||
}
|
||||
|
||||
var ErrDataConflict = Define("dataConflict")
|
||||
|
||||
func DataConflict(msg string) error {
|
||||
return Wrap(ErrDataConflict, msg)
|
||||
}
|
||||
|
||||
func DataConflictf(format string, args ...any) error {
|
||||
return DataConflict(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func DataConflictWrap(err error, msg string) error {
|
||||
return wrapError(ErrDataConflict, msg, err)
|
||||
}
|
||||
|
||||
func DataConflictWrapf(err error, format string, args ...any) error {
|
||||
return DataConflictWrap(err, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
var ErrFailedPrecondition = Define("failedPrecondition")
|
||||
|
||||
func FailedPrecondition(msg string) error {
|
||||
return Wrap(ErrFailedPrecondition, msg)
|
||||
}
|
||||
|
||||
func FailedPreconditionf(format string, args ...any) error {
|
||||
return FailedPrecondition(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func FailedPreconditionWrap(err error, msg string) error {
|
||||
return wrapError(ErrFailedPrecondition, msg, err)
|
||||
}
|
||||
|
||||
func FailedPreconditionWrapf(err error, format string, args ...any) error {
|
||||
return FailedPreconditionWrap(err, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
var ErrServiceUnavailable = Define("serviceUnavailable")
|
||||
|
||||
func ServiceUnavailable(msg string) error {
|
||||
return Wrap(ErrServiceUnavailable, msg)
|
||||
}
|
||||
|
||||
func ServiceUnavailablef(format string, args ...any) error {
|
||||
return ServiceUnavailable(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func ServiceUnavailableWrap(err error, msg string) error {
|
||||
return wrapError(ErrServiceUnavailable, msg, err)
|
||||
}
|
||||
|
||||
func ServiceUnavailableWrapf(err error, format string, args ...any) error {
|
||||
return ServiceUnavailableWrap(err, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
type objectRef interface {
|
||||
IsZero() bool
|
||||
Hex() string
|
||||
}
|
||||
|
||||
var ErrAccessDenied = Define("accessDenied")
|
||||
|
||||
func PermissionDenied(msg string) error {
|
||||
return Wrap(ErrAccessDenied, msg)
|
||||
}
|
||||
|
||||
func AccessDenied(object, action string, ref objectRef) error {
|
||||
if ref != nil && !ref.IsZero() {
|
||||
return Wrap(ErrAccessDenied, fmt.Sprintf("cannot %s object of type '%s' with ID '%s'", action, object, ref.Hex()))
|
||||
}
|
||||
return Wrap(ErrAccessDenied, fmt.Sprintf("cannot %s object of type '%s'", action, object))
|
||||
}
|
||||
|
||||
var ErrInvalidDataType = Define("invalidDataType")
|
||||
|
||||
func InvalidDataType(msg string) error {
|
||||
return Wrap(ErrInvalidDataType, msg)
|
||||
}
|
||||
|
||||
func InvalidDataTypef(format string, args ...any) error {
|
||||
return InvalidDataType(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
var ErrUnauthorized = Define("unathorized")
|
||||
|
||||
func Unauthorized(msg string) error {
|
||||
return Wrap(ErrUnauthorized, msg)
|
||||
}
|
||||
|
||||
func Unauthorizedf(format string, args ...any) error {
|
||||
return Unauthorized(fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
var ErrNoMessagingTopic = Define("messagingTopicError")
|
||||
|
||||
func NoMessagingTopic(topic string) error {
|
||||
return Wrapf(ErrNoMessagingTopic, "messaging topic '%s' not found", topic)
|
||||
}
|
||||
|
||||
func InvalidArgumentWrap(err error, msg string, argumentNames ...string) error {
|
||||
return wrapError(ErrInvalidArg, invalidArgumentMessage(msg, argumentNames...), err)
|
||||
}
|
||||
|
||||
func InvalidArgumentWrapf(err error, format string, args ...any) error {
|
||||
return InvalidArgumentWrap(err, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func InternalWrap(err error, msg string) error {
|
||||
return wrapError(ErrInternal, msg, err)
|
||||
}
|
||||
|
||||
func InternalWrapf(err error, format string, args ...any) error {
|
||||
return InternalWrap(err, fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
func InternalWrapAll(msg string, errs ...error) error {
|
||||
return wrapErrors(ErrInternal, msg, errs...)
|
||||
}
|
||||
|
||||
func wrapError(base error, msg string, err error) error {
|
||||
return wrapErrors(base, msg, err)
|
||||
}
|
||||
|
||||
func wrapErrors(base error, msg string, errs ...error) error {
|
||||
baseErr := Wrap(base, msg)
|
||||
causes := make([]error, 0, len(errs))
|
||||
for _, err := range errs {
|
||||
if err != nil {
|
||||
causes = append(causes, err)
|
||||
}
|
||||
}
|
||||
if len(causes) == 0 {
|
||||
return baseErr
|
||||
}
|
||||
return errors.Join(baseErr, errors.Join(causes...))
|
||||
}
|
||||
@@ -0,0 +1,125 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
type testReasonedError struct {
|
||||
error
|
||||
reason string
|
||||
}
|
||||
|
||||
func (e testReasonedError) Reason() string {
|
||||
return e.reason
|
||||
}
|
||||
|
||||
func TestReasonUsesReasonedError(t *testing.T) {
|
||||
err := testReasonedError{
|
||||
error: InvalidArgument("value is invalid"),
|
||||
reason: "amount_below_minimum",
|
||||
}
|
||||
reason, ok := Reason(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected reason")
|
||||
}
|
||||
if reason != "amount_below_minimum" {
|
||||
t.Fatalf("reason mismatch: got=%q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidArgumentSupportsBrokenArgumentName(t *testing.T) {
|
||||
t.Run("without argument name keeps old behavior", func(t *testing.T) {
|
||||
err := InvalidArgument("value is missing")
|
||||
expected := "invalidArgError: value is missing"
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("unexpected error message: %s", err)
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidArg) {
|
||||
t.Fatalf("error should wrap ErrInvalidArg")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("single argument name", func(t *testing.T) {
|
||||
err := InvalidArgument("value is missing", "bot_token_env")
|
||||
expected := `invalidArgError: broken argument "bot_token_env": value is missing`
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("unexpected error message: %s", err)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("multiple argument names", func(t *testing.T) {
|
||||
err := InvalidArgument("value mismatch", "bot_token_env", "chat_id_env", " ")
|
||||
expected := `invalidArgError: broken arguments "bot_token_env", "chat_id_env": value mismatch`
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("unexpected error message: %s", err)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestInvalidArgumentWithNamesfSupportsBrokenArgumentName(t *testing.T) {
|
||||
err := InvalidArgumentWithNamesf([]string{"gateway.supported_assets.currency"}, "asset[%d] %s", 2, "is invalid")
|
||||
expected := `invalidArgError: broken argument "gateway.supported_assets.currency": asset[2] is invalid`
|
||||
if err.Error() != expected {
|
||||
t.Fatalf("unexpected error message: %s", err)
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidArg) {
|
||||
t.Fatalf("error should wrap ErrInvalidArg")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInvalidArgumentWrapSupportsBrokenArgumentName(t *testing.T) {
|
||||
base := Define("root cause")
|
||||
err := InvalidArgumentWrap(base, "value is missing", "bot_token_env")
|
||||
if !strings.Contains(err.Error(), `invalidArgError: broken argument "bot_token_env": value is missing`) {
|
||||
t.Fatalf("wrapped error should include broken argument name: %s", err)
|
||||
}
|
||||
if !errors.Is(err, ErrInvalidArg) || !errors.Is(err, base) {
|
||||
t.Fatalf("wrapped error should preserve all error layers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestInternalWrapAllPreservesAllCauses(t *testing.T) {
|
||||
first := Define("first cause")
|
||||
second := Define("second cause")
|
||||
err := InternalWrapAll("combined failure", first, nil, second)
|
||||
if !strings.Contains(err.Error(), "internal: combined failure") {
|
||||
t.Fatalf("wrapped error should include context: %s", err)
|
||||
}
|
||||
if !errors.Is(err, ErrInternal) || !errors.Is(err, first) || !errors.Is(err, second) {
|
||||
t.Fatalf("wrapped error should preserve all error layers")
|
||||
}
|
||||
}
|
||||
|
||||
func TestOpaqueWrapPreservesCauseWithoutExposingItsText(t *testing.T) {
|
||||
cause := DataConflict("restricted storage key")
|
||||
err := OpaqueWrap(ErrInternal, cause, "safe persistence failure")
|
||||
|
||||
if got, want := err.Error(), "internal: safe persistence failure"; got != want {
|
||||
t.Fatalf("opaque error mismatch: got=%q want=%q", got, want)
|
||||
}
|
||||
if !errors.Is(err, ErrInternal) || !errors.Is(err, ErrDataConflict) {
|
||||
t.Fatalf("opaque error did not preserve classifications: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestOperationalWrapsPreserveClassificationAndCause(t *testing.T) {
|
||||
cause := Define("dependency failure")
|
||||
tests := []struct {
|
||||
name string
|
||||
err error
|
||||
kind error
|
||||
}{
|
||||
{name: "failed precondition", err: FailedPreconditionWrap(cause, "AA capability unavailable"), kind: ErrFailedPrecondition},
|
||||
{name: "service unavailable", err: ServiceUnavailableWrap(cause, "Arbitrum RPC unavailable"), kind: ErrServiceUnavailable},
|
||||
{name: "data conflict", err: DataConflictWrap(cause, "managed wallet replay differs"), kind: ErrDataConflict},
|
||||
}
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if !errors.Is(tt.err, tt.kind) || !errors.Is(tt.err, cause) {
|
||||
t.Fatalf("wrapped error should preserve classification and cause: %s", tt.err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// InvalidArgumentClassified returns an invalid-argument error with a stable
|
||||
// machine reason and structured field names for transport adapters.
|
||||
func InvalidArgumentClassified(reason, msg string, argumentNames ...string) error {
|
||||
return classifiedError{
|
||||
base: ErrInvalidArg,
|
||||
msg: invalidArgumentMessage(msg, argumentNames...),
|
||||
reason: strings.TrimSpace(reason),
|
||||
argumentNames: normalizeArgumentNames(argumentNames),
|
||||
}
|
||||
}
|
||||
|
||||
func invalidArgumentMessage(msg string, argumentNames ...string) string {
|
||||
normalizedNames := normalizeArgumentNames(argumentNames)
|
||||
names := make([]string, 0, len(normalizedNames))
|
||||
for _, name := range normalizedNames {
|
||||
names = append(names, fmt.Sprintf("%q", name))
|
||||
}
|
||||
if len(names) == 0 {
|
||||
return msg
|
||||
}
|
||||
|
||||
prefix := "broken argument"
|
||||
if len(names) > 1 {
|
||||
prefix = "broken arguments"
|
||||
}
|
||||
return fmt.Sprintf("%s %s: %s", prefix, strings.Join(names, ", "), msg)
|
||||
}
|
||||
|
||||
func normalizeArgumentNames(argumentNames []string) []string {
|
||||
names := make([]string, 0, len(argumentNames))
|
||||
for _, name := range argumentNames {
|
||||
if name = strings.TrimSpace(name); name != "" {
|
||||
names = append(names, name)
|
||||
}
|
||||
}
|
||||
return names
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
const (
|
||||
ReasonLedgerIdempotencyConflict = "LEDGER_IDEMPOTENCY_CONFLICT"
|
||||
ReasonLedgerCommandIdentityMismatch = "LEDGER_COMMAND_IDENTITY_MISMATCH"
|
||||
ReasonLedgerAccessViolation = "LEDGER_ACCESS_VIOLATION"
|
||||
ReasonAccountNotEligible = "ACCOUNT_NOT_ELIGIBLE"
|
||||
)
|
||||
|
||||
var ErrLedgerIdempotencyConflict = Define("ledger.idempotency_conflict")
|
||||
|
||||
var ErrLedgerCommandIdentityMismatch = Define("ledger.command_identity_mismatch")
|
||||
|
||||
var ErrLedgerAccessViolation = Define("ledger.access_violation")
|
||||
|
||||
var ErrAccountNotEligible = Define("account_not_eligible")
|
||||
|
||||
func LedgerIdempotencyConflict(msg string) error {
|
||||
return classifiedError{
|
||||
base: ErrDataConflict,
|
||||
code: ErrLedgerIdempotencyConflict,
|
||||
msg: ledgerErrorMessage(msg, ErrLedgerIdempotencyConflict),
|
||||
reason: ReasonLedgerIdempotencyConflict,
|
||||
}
|
||||
}
|
||||
|
||||
func LedgerCommandIdentityMismatch(msg string) error {
|
||||
return classifiedError{
|
||||
base: ErrDataConflict,
|
||||
code: ErrLedgerCommandIdentityMismatch,
|
||||
msg: ledgerErrorMessage(msg, ErrLedgerCommandIdentityMismatch),
|
||||
reason: ReasonLedgerCommandIdentityMismatch,
|
||||
}
|
||||
}
|
||||
|
||||
func LedgerAccessViolation(msg string) error {
|
||||
return classifiedError{
|
||||
base: ErrFailedPrecondition,
|
||||
code: ErrLedgerAccessViolation,
|
||||
msg: ledgerErrorMessage(msg, ErrLedgerAccessViolation),
|
||||
reason: ReasonLedgerAccessViolation,
|
||||
}
|
||||
}
|
||||
|
||||
func AccountNotEligible(msg string) error {
|
||||
return classifiedError{
|
||||
base: ErrFailedPrecondition,
|
||||
code: ErrAccountNotEligible,
|
||||
msg: ledgerErrorMessage(msg, ErrAccountNotEligible),
|
||||
reason: ReasonAccountNotEligible,
|
||||
}
|
||||
}
|
||||
|
||||
func ledgerErrorMessage(msg string, fallback error) string {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg != "" {
|
||||
return msg
|
||||
}
|
||||
if fallback == nil {
|
||||
return ""
|
||||
}
|
||||
return fallback.Error()
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestLedgerReasonRoundTrip(t *testing.T) {
|
||||
err := LedgerIdempotencyConflict("conflicting command")
|
||||
reason, ok := Reason(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ReasonLedgerIdempotencyConflict, reason)
|
||||
|
||||
restored, ok := FromReason(reason, "conflicting command")
|
||||
require.True(t, ok)
|
||||
require.True(t, errors.Is(restored, ErrLedgerIdempotencyConflict))
|
||||
require.True(t, errors.Is(restored, ErrDataConflict))
|
||||
}
|
||||
|
||||
func TestAccountNotEligibleReasonRoundTrip(t *testing.T) {
|
||||
err := AccountNotEligible("account is frozen")
|
||||
reason, ok := Reason(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ReasonAccountNotEligible, reason)
|
||||
require.True(t, errors.Is(err, ErrAccountNotEligible))
|
||||
require.True(t, errors.Is(err, ErrFailedPrecondition))
|
||||
|
||||
restored, ok := FromReason(reason, "account is frozen")
|
||||
require.True(t, ok)
|
||||
require.True(t, errors.Is(restored, ErrAccountNotEligible))
|
||||
require.True(t, errors.Is(restored, ErrFailedPrecondition))
|
||||
}
|
||||
|
||||
func TestLedgerAccessViolationReasonRoundTrip(t *testing.T) {
|
||||
err := LedgerAccessViolation("from_account: account does not belong to organization")
|
||||
reason, ok := Reason(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, ReasonLedgerAccessViolation, reason)
|
||||
require.True(t, errors.Is(err, ErrLedgerAccessViolation))
|
||||
require.True(t, errors.Is(err, ErrFailedPrecondition))
|
||||
|
||||
restored, ok := FromReason(reason, "from_account: account does not belong to organization")
|
||||
require.True(t, ok)
|
||||
require.True(t, errors.Is(restored, ErrLedgerAccessViolation))
|
||||
require.True(t, errors.Is(restored, ErrFailedPrecondition))
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package merrors
|
||||
|
||||
var ErrLimitsRuntimeInvalidRecord = Define("limits.runtime.invalid_record")
|
||||
|
||||
var ErrLimitsRuntimeCASConflict = Define("limits.runtime.cas_conflict")
|
||||
|
||||
var ErrLimitsRuntimeDuplicateStateKey = Define("limits.runtime.duplicate_state_key")
|
||||
|
||||
var ErrLimitsRuntimeDuplicateStateRef = Define("limits.runtime.duplicate_state_ref")
|
||||
|
||||
var ErrLimitsRuntimeDuplicateReservationRef = Define("limits.runtime.duplicate_reservation_ref")
|
||||
|
||||
var ErrLimitsRuntimeDuplicateReservationOperation = Define("limits.runtime.duplicate_reservation_operation")
|
||||
|
||||
var ErrLimitsRuntimeIdentityConflict = Define("limits.runtime.identity_conflict")
|
||||
|
||||
var ErrLimitsRuntimeDataConflict = Define("limits.runtime.data_conflict")
|
||||
|
||||
var ErrLimitsRuntimeRepositoryRequired = Define("limits.runtime.repository_required")
|
||||
@@ -0,0 +1,66 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
// Reasons for organization provisioning outcomes. They travel to callers in
|
||||
// gRPC error details so every outcome stays distinguishable regardless of the
|
||||
// status code it shares with others.
|
||||
const (
|
||||
ReasonOrganizationProvisioningInProgress = "organization.provisioning_in_progress"
|
||||
ReasonOrganizationAlreadyActive = "organization.already_active"
|
||||
ReasonOrganizationProvisioningBlocked = "organization.provisioning_blocked"
|
||||
ReasonOrganizationOwnerMismatch = "organization.owner_mismatch"
|
||||
ReasonOrganizationNotOperatorProvisioned = "organization.not_operator_provisioned"
|
||||
)
|
||||
|
||||
// ErrOrganizationProvisioningInProgress means another attempt currently holds
|
||||
// the provisioning lease of the organization.
|
||||
var ErrOrganizationProvisioningInProgress = Define(ReasonOrganizationProvisioningInProgress)
|
||||
|
||||
// ErrOrganizationAlreadyActive means the organization finished provisioning already.
|
||||
var ErrOrganizationAlreadyActive = Define(ReasonOrganizationAlreadyActive)
|
||||
|
||||
// ErrOrganizationProvisioningBlocked means a failed signup compensated the
|
||||
// organization and it requires manual recovery before it can be activated.
|
||||
var ErrOrganizationProvisioningBlocked = Define(ReasonOrganizationProvisioningBlocked)
|
||||
|
||||
// ErrOrganizationOwnerMismatch means the organization is being provisioned for
|
||||
// a different account than the one supplied.
|
||||
var ErrOrganizationOwnerMismatch = Define(ReasonOrganizationOwnerMismatch)
|
||||
|
||||
// ErrOrganizationNotOperatorProvisioned means the organization was created through signup:
|
||||
// organizations created through general signup are never finished by someone in backoffice.
|
||||
var ErrOrganizationNotOperatorProvisioned = Define(ReasonOrganizationNotOperatorProvisioned)
|
||||
|
||||
func OrganizationProvisioningInProgress(msg string) error {
|
||||
return organizationError(ErrOrganizationProvisioningInProgress, ReasonOrganizationProvisioningInProgress, msg)
|
||||
}
|
||||
|
||||
func OrganizationAlreadyActive(msg string) error {
|
||||
return organizationError(ErrOrganizationAlreadyActive, ReasonOrganizationAlreadyActive, msg)
|
||||
}
|
||||
|
||||
func OrganizationProvisioningBlocked(msg string) error {
|
||||
return organizationError(ErrOrganizationProvisioningBlocked, ReasonOrganizationProvisioningBlocked, msg)
|
||||
}
|
||||
|
||||
func OrganizationOwnerMismatch(msg string) error {
|
||||
return organizationError(ErrOrganizationOwnerMismatch, ReasonOrganizationOwnerMismatch, msg)
|
||||
}
|
||||
|
||||
func OrganizationNotOperatorProvisioned(msg string) error {
|
||||
return organizationError(ErrOrganizationNotOperatorProvisioned, ReasonOrganizationNotOperatorProvisioned, msg)
|
||||
}
|
||||
|
||||
func organizationError(code error, reason, msg string) error {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg == "" && code != nil {
|
||||
msg = code.Error()
|
||||
}
|
||||
return classifiedError{
|
||||
base: ErrDataConflict,
|
||||
code: code,
|
||||
msg: msg,
|
||||
reason: reason,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,9 @@
|
||||
package merrors
|
||||
|
||||
var ErrPaymasterPaused = Define("paymaster_paused")
|
||||
|
||||
var ErrControlWalletInsufficientNativeBalance = Define("control_wallet_insufficient_native_balance")
|
||||
|
||||
var ErrPaymasterControlDurableWalletFailure = Define("paymaster_control_durable_wallet_failure")
|
||||
|
||||
var ErrPaymasterControlKeyActivityMismatch = Define("paymaster_control_key_activity_mismatch")
|
||||
@@ -0,0 +1,47 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
const ReasonPaymentExecutionNotAccepted = "PAYMENT_EXECUTION_NOT_ACCEPTED"
|
||||
|
||||
var ErrPaymentExecutionNotAccepted = Define("payment.execution_not_accepted")
|
||||
|
||||
type paymentExecutionNotAcceptedError struct {
|
||||
classifiedError
|
||||
cause error
|
||||
}
|
||||
|
||||
func (e paymentExecutionNotAcceptedError) Unwrap() []error {
|
||||
causes := e.classifiedError.Unwrap()
|
||||
if e.cause != nil {
|
||||
causes = append(causes, e.cause)
|
||||
}
|
||||
return causes
|
||||
}
|
||||
|
||||
// PaymentExecutionNotAccepted reports that the orchestrator proved an
|
||||
// execution request did not create or reuse any payment. Callers may release
|
||||
// request-side reservations only for this outcome; ordinary transport and
|
||||
// internal errors have an ambiguous ownership result.
|
||||
func PaymentExecutionNotAccepted(msg string) error {
|
||||
return PaymentExecutionNotAcceptedWrap(nil, msg)
|
||||
}
|
||||
|
||||
// PaymentExecutionNotAcceptedWrap adds the definitive rejection
|
||||
// classification while retaining the underlying cause for diagnostics.
|
||||
func PaymentExecutionNotAcceptedWrap(cause error, msg string) error {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg == "" {
|
||||
msg = "payment execution was not accepted before a payment was created"
|
||||
}
|
||||
return paymentExecutionNotAcceptedError{
|
||||
classifiedError: classifiedError{
|
||||
base: ErrFailedPrecondition,
|
||||
code: ErrPaymentExecutionNotAccepted,
|
||||
msg: msg,
|
||||
reason: ReasonPaymentExecutionNotAccepted,
|
||||
publicMessage: true,
|
||||
},
|
||||
cause: cause,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,26 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestPaymentExecutionNotAcceptedPreservesClassificationAndCause(t *testing.T) {
|
||||
cause := ServiceUnavailable("orchestrator unavailable")
|
||||
err := PaymentExecutionNotAcceptedWrap(cause, "execution was rejected")
|
||||
|
||||
if !errors.Is(err, ErrFailedPrecondition) ||
|
||||
!errors.Is(err, ErrPaymentExecutionNotAccepted) ||
|
||||
!errors.Is(err, cause) {
|
||||
t.Fatalf("PaymentExecutionNotAcceptedWrap() classification = %v", err)
|
||||
}
|
||||
reason, ok := Reason(err)
|
||||
if !ok || reason != ReasonPaymentExecutionNotAccepted {
|
||||
t.Fatalf("PaymentExecutionNotAcceptedWrap() reason = %q, %t", reason, ok)
|
||||
}
|
||||
restored, ok := FromReason(reason, err.Error())
|
||||
if !ok || !errors.Is(restored, ErrPaymentExecutionNotAccepted) ||
|
||||
!errors.Is(restored, ErrFailedPrecondition) {
|
||||
t.Fatalf("FromReason() = %v, %t", restored, ok)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package merrors
|
||||
|
||||
const (
|
||||
QuotationFeeConfigurationMissingCode = "quotation_fee_configuration_missing"
|
||||
QuotationFeeConfigurationMissingDetails = "quotation fee configuration is missing"
|
||||
QuotationReplayMissingCode = "quotation_replay_missing"
|
||||
QuotationReplayMissingDetails = "the expected payment quotation is no longer available"
|
||||
)
|
||||
|
||||
var ErrQuotationFeeConfigurationMissing = Define(QuotationFeeConfigurationMissingCode)
|
||||
|
||||
var ErrQuotationReplayMissing = Define(QuotationReplayMissingCode)
|
||||
|
||||
func QuotationFeeConfigurationMissing() error {
|
||||
return classifiedError{
|
||||
base: ErrFailedPrecondition,
|
||||
code: ErrQuotationFeeConfigurationMissing,
|
||||
msg: QuotationFeeConfigurationMissingDetails,
|
||||
reason: QuotationFeeConfigurationMissingCode,
|
||||
publicMessage: true,
|
||||
}
|
||||
}
|
||||
|
||||
func QuotationReplayMissing() error {
|
||||
return classifiedError{
|
||||
base: ErrNoData,
|
||||
code: ErrQuotationReplayMissing,
|
||||
msg: QuotationReplayMissingDetails,
|
||||
reason: QuotationReplayMissingCode,
|
||||
publicMessage: true,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestQuotationFeeConfigurationMissingReasonRoundTrip(t *testing.T) {
|
||||
err := QuotationFeeConfigurationMissing()
|
||||
|
||||
require.True(t, errors.Is(err, ErrFailedPrecondition))
|
||||
require.True(t, errors.Is(err, ErrQuotationFeeConfigurationMissing))
|
||||
|
||||
reason, ok := Reason(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, QuotationFeeConfigurationMissingCode, reason)
|
||||
require.Equal(t, QuotationFeeConfigurationMissingDetails, err.Error())
|
||||
|
||||
restored, ok := FromReason(reason, "ignored message")
|
||||
require.True(t, ok)
|
||||
require.True(t, errors.Is(restored, ErrFailedPrecondition))
|
||||
require.True(t, errors.Is(restored, ErrQuotationFeeConfigurationMissing))
|
||||
require.Equal(t, QuotationFeeConfigurationMissingDetails, restored.Error())
|
||||
}
|
||||
|
||||
func TestQuotationReplayMissingReasonRoundTrip(t *testing.T) {
|
||||
err := QuotationReplayMissing()
|
||||
|
||||
require.True(t, errors.Is(err, ErrNoData))
|
||||
require.True(t, errors.Is(err, ErrQuotationReplayMissing))
|
||||
|
||||
reason, ok := Reason(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, QuotationReplayMissingCode, reason)
|
||||
require.Equal(t, QuotationReplayMissingDetails, err.Error())
|
||||
|
||||
restored, ok := FromReason(reason, "ignored message")
|
||||
require.True(t, ok)
|
||||
require.True(t, errors.Is(restored, ErrNoData))
|
||||
require.True(t, errors.Is(restored, ErrQuotationReplayMissing))
|
||||
require.Equal(t, QuotationReplayMissingDetails, restored.Error())
|
||||
}
|
||||
@@ -0,0 +1,105 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
func Reason(err error) (string, bool) {
|
||||
var reasoned interface {
|
||||
Reason() string
|
||||
}
|
||||
if errors.As(err, &reasoned) {
|
||||
reason := strings.TrimSpace(reasoned.Reason())
|
||||
if reason != "" {
|
||||
return reason, true
|
||||
}
|
||||
}
|
||||
switch {
|
||||
case errors.Is(err, ErrAMLSourceIdempotencyConflict):
|
||||
return ReasonAMLSourceIdempotencyConflict, true
|
||||
case errors.Is(err, ErrLedgerIdempotencyConflict):
|
||||
return ReasonLedgerIdempotencyConflict, true
|
||||
case errors.Is(err, ErrLedgerCommandIdentityMismatch):
|
||||
return ReasonLedgerCommandIdentityMismatch, true
|
||||
case errors.Is(err, ErrLedgerAccessViolation):
|
||||
return ReasonLedgerAccessViolation, true
|
||||
case errors.Is(err, ErrAccountNotEligible):
|
||||
return ReasonAccountNotEligible, true
|
||||
case errors.Is(err, ErrQuotationFeeConfigurationMissing):
|
||||
return QuotationFeeConfigurationMissingCode, true
|
||||
case errors.Is(err, ErrQuotationReplayMissing):
|
||||
return QuotationReplayMissingCode, true
|
||||
default:
|
||||
return "", false
|
||||
}
|
||||
}
|
||||
|
||||
func FromReason(reason string, msg string) (error, bool) {
|
||||
switch strings.TrimSpace(reason) {
|
||||
case ReasonRouteNotFound:
|
||||
return RouteNotFound(msg), true
|
||||
case ReasonRouteAmbiguous:
|
||||
return RouteAmbiguous(msg), true
|
||||
case ReasonRouteDisabled:
|
||||
return RouteDisabled(msg), true
|
||||
case ReasonRouteInvalidSource:
|
||||
return RouteInvalidSource(msg), true
|
||||
case ReasonRouteDestinationNotFound:
|
||||
return RouteDestinationNotFound(msg), true
|
||||
case ReasonRouteDestinationInactive:
|
||||
return RouteDestinationInactive(msg), true
|
||||
case ReasonRouteDestinationCurrencyMismatch:
|
||||
return RouteDestinationCurrencyMismatch(msg), true
|
||||
case ReasonRouteDestinationOrgMismatch:
|
||||
return RouteDestinationOrgMismatch(msg), true
|
||||
case ReasonRouteDestinationAmbiguous:
|
||||
return RouteDestinationAmbiguous(msg), true
|
||||
case ReasonRouteDestinationOwnerMismatch:
|
||||
return RouteDestinationOwnerMismatch(msg), true
|
||||
case ReasonRouteActiveSourceConflict:
|
||||
return RouteActiveSourceConflict(msg), true
|
||||
case ReasonRouteDestinationTypeMismatch:
|
||||
return RouteDestinationTypeMismatch(msg), true
|
||||
case ReasonRouteDestinationRoleMismatch:
|
||||
return RouteDestinationRoleMismatch(msg), true
|
||||
case ReasonRouteIdempotencyConflict:
|
||||
return RouteIdempotencyConflict(msg), true
|
||||
case ReasonRouteVersionConflict:
|
||||
return RouteVersionConflict(msg), true
|
||||
case ReasonRouteInvalidState:
|
||||
return RouteInvalidState(msg), true
|
||||
case ReasonAMLSourceIdempotencyConflict:
|
||||
return AMLSourceIdempotencyConflict(msg), true
|
||||
case ReasonLedgerIdempotencyConflict:
|
||||
return LedgerIdempotencyConflict(msg), true
|
||||
case ReasonLedgerCommandIdentityMismatch:
|
||||
return LedgerCommandIdentityMismatch(msg), true
|
||||
case ReasonLedgerAccessViolation:
|
||||
return LedgerAccessViolation(msg), true
|
||||
case ReasonAccountNotEligible:
|
||||
return AccountNotEligible(msg), true
|
||||
case ReasonDocumentsReportOwned:
|
||||
return DocumentsReportOwned(msg), true
|
||||
case ReasonDocumentsQuotationExpired:
|
||||
return DocumentsQuotationExpired(msg), true
|
||||
case ReasonPaymentExecutionNotAccepted:
|
||||
return PaymentExecutionNotAccepted(msg), true
|
||||
case QuotationFeeConfigurationMissingCode:
|
||||
return QuotationFeeConfigurationMissing(), true
|
||||
case QuotationReplayMissingCode:
|
||||
return QuotationReplayMissing(), true
|
||||
case ReasonOrganizationProvisioningInProgress:
|
||||
return OrganizationProvisioningInProgress(msg), true
|
||||
case ReasonOrganizationAlreadyActive:
|
||||
return OrganizationAlreadyActive(msg), true
|
||||
case ReasonOrganizationProvisioningBlocked:
|
||||
return OrganizationProvisioningBlocked(msg), true
|
||||
case ReasonOrganizationOwnerMismatch:
|
||||
return OrganizationOwnerMismatch(msg), true
|
||||
case ReasonOrganizationNotOperatorProvisioned:
|
||||
return OrganizationNotOperatorProvisioned(msg), true
|
||||
default:
|
||||
return nil, false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,127 @@
|
||||
package merrors
|
||||
|
||||
import "strings"
|
||||
|
||||
const routeErrorCodePrefix = "ROUTE_ERROR_CODE_"
|
||||
|
||||
type routeMetadataError struct {
|
||||
classifiedError
|
||||
routeCode string
|
||||
}
|
||||
|
||||
func (e routeMetadataError) ErrorInfoMetadata() map[string]string {
|
||||
return map[string]string{"route_error_code": e.routeCode}
|
||||
}
|
||||
|
||||
const (
|
||||
ReasonRouteNotFound = "ROUTE_NOT_FOUND"
|
||||
ReasonRouteAmbiguous = "ROUTE_AMBIGUOUS"
|
||||
ReasonRouteDisabled = "ROUTE_DISABLED"
|
||||
ReasonRouteInvalidSource = "ROUTE_INVALID_SOURCE"
|
||||
ReasonRouteDestinationNotFound = "ROUTE_DESTINATION_NOT_FOUND"
|
||||
ReasonRouteDestinationInactive = "ROUTE_DESTINATION_INACTIVE"
|
||||
ReasonRouteDestinationCurrencyMismatch = "ROUTE_DESTINATION_CURRENCY_MISMATCH"
|
||||
ReasonRouteDestinationOrgMismatch = "ROUTE_DESTINATION_ORG_MISMATCH"
|
||||
ReasonRouteDestinationAmbiguous = "ROUTE_DESTINATION_AMBIGUOUS"
|
||||
ReasonRouteDestinationOwnerMismatch = "ROUTE_DESTINATION_OWNER_MISMATCH"
|
||||
ReasonRouteActiveSourceConflict = "ROUTE_ACTIVE_SOURCE_CONFLICT"
|
||||
ReasonRouteDestinationTypeMismatch = "ROUTE_DESTINATION_TYPE_MISMATCH"
|
||||
ReasonRouteDestinationRoleMismatch = "ROUTE_DESTINATION_ROLE_MISMATCH"
|
||||
ReasonRouteIdempotencyConflict = "ROUTE_IDEMPOTENCY_CONFLICT"
|
||||
ReasonRouteVersionConflict = "ROUTE_VERSION_CONFLICT"
|
||||
ReasonRouteInvalidState = "ROUTE_INVALID_STATE"
|
||||
)
|
||||
|
||||
var (
|
||||
ErrRouteNotFound = Define("route.not_found")
|
||||
ErrRouteAmbiguous = Define("route.ambiguous")
|
||||
ErrRouteDisabled = Define("route.disabled")
|
||||
ErrRouteInvalidSource = Define("route.invalid_source")
|
||||
ErrRouteDestinationNotFound = Define("route.destination_not_found")
|
||||
ErrRouteDestinationInactive = Define("route.destination_inactive")
|
||||
ErrRouteDestinationCurrencyMismatch = Define("route.destination_currency_mismatch")
|
||||
ErrRouteDestinationOrgMismatch = Define("route.destination_org_mismatch")
|
||||
ErrRouteDestinationAmbiguous = Define("route.destination_ambiguous")
|
||||
ErrRouteDestinationOwnerMismatch = Define("route.destination_owner_mismatch")
|
||||
ErrRouteActiveSourceConflict = Define("route.active_source_conflict")
|
||||
ErrRouteDestinationTypeMismatch = Define("route.destination_type_mismatch")
|
||||
ErrRouteDestinationRoleMismatch = Define("route.destination_role_mismatch")
|
||||
ErrRouteIdempotencyConflict = Define("route.idempotency_conflict")
|
||||
ErrRouteVersionConflict = Define("route.version_conflict")
|
||||
ErrRouteInvalidState = Define("route.invalid_state")
|
||||
)
|
||||
|
||||
func RouteNotFound(msg string) error {
|
||||
return newRouteClassifiedError(ErrNoData, ErrRouteNotFound, ReasonRouteNotFound, msg)
|
||||
}
|
||||
|
||||
func RouteAmbiguous(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteAmbiguous, ReasonRouteAmbiguous, msg)
|
||||
}
|
||||
|
||||
func RouteDisabled(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDisabled, ReasonRouteDisabled, msg)
|
||||
}
|
||||
|
||||
func RouteInvalidSource(msg string) error {
|
||||
return newRouteClassifiedError(ErrInvalidArg, ErrRouteInvalidSource, ReasonRouteInvalidSource, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationNotFound(msg string) error {
|
||||
return newRouteClassifiedError(ErrNoData, ErrRouteDestinationNotFound, ReasonRouteDestinationNotFound, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationInactive(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationInactive, ReasonRouteDestinationInactive, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationCurrencyMismatch(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationCurrencyMismatch, ReasonRouteDestinationCurrencyMismatch, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationOrgMismatch(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationOrgMismatch, ReasonRouteDestinationOrgMismatch, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationAmbiguous(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationAmbiguous, ReasonRouteDestinationAmbiguous, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationOwnerMismatch(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationOwnerMismatch, ReasonRouteDestinationOwnerMismatch, msg)
|
||||
}
|
||||
|
||||
func RouteActiveSourceConflict(msg string) error {
|
||||
return newRouteClassifiedError(ErrDataConflict, ErrRouteActiveSourceConflict, ReasonRouteActiveSourceConflict, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationTypeMismatch(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationTypeMismatch, ReasonRouteDestinationTypeMismatch, msg)
|
||||
}
|
||||
|
||||
func RouteDestinationRoleMismatch(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteDestinationRoleMismatch, ReasonRouteDestinationRoleMismatch, msg)
|
||||
}
|
||||
|
||||
func RouteIdempotencyConflict(msg string) error {
|
||||
return newRouteClassifiedError(ErrDataConflict, ErrRouteIdempotencyConflict, ReasonRouteIdempotencyConflict, msg)
|
||||
}
|
||||
|
||||
func RouteVersionConflict(msg string) error {
|
||||
return newRouteClassifiedError(ErrDataConflict, ErrRouteVersionConflict, ReasonRouteVersionConflict, msg)
|
||||
}
|
||||
|
||||
func RouteInvalidState(msg string) error {
|
||||
return newRouteClassifiedError(ErrFailedPrecondition, ErrRouteInvalidState, ReasonRouteInvalidState, msg)
|
||||
}
|
||||
|
||||
func newRouteClassifiedError(base, code error, reason, msg string) error {
|
||||
msg = strings.TrimSpace(msg)
|
||||
if msg == "" && code != nil {
|
||||
msg = code.Error()
|
||||
}
|
||||
return routeMetadataError{
|
||||
classifiedError: classifiedError{base: base, code: code, msg: msg, reason: reason, publicMessage: true},
|
||||
routeCode: routeErrorCodePrefix + reason,
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
)
|
||||
|
||||
func TestRouteReasonsRoundTrip(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
create func(string) error
|
||||
code error
|
||||
base error
|
||||
reason string
|
||||
}{
|
||||
{name: "not found", create: RouteNotFound, code: ErrRouteNotFound, base: ErrNoData, reason: ReasonRouteNotFound},
|
||||
{name: "ambiguous", create: RouteAmbiguous, code: ErrRouteAmbiguous, base: ErrFailedPrecondition, reason: ReasonRouteAmbiguous},
|
||||
{name: "disabled", create: RouteDisabled, code: ErrRouteDisabled, base: ErrFailedPrecondition, reason: ReasonRouteDisabled},
|
||||
{name: "invalid source", create: RouteInvalidSource, code: ErrRouteInvalidSource, base: ErrInvalidArg, reason: ReasonRouteInvalidSource},
|
||||
{name: "destination not found", create: RouteDestinationNotFound, code: ErrRouteDestinationNotFound, base: ErrNoData, reason: ReasonRouteDestinationNotFound},
|
||||
{name: "destination inactive", create: RouteDestinationInactive, code: ErrRouteDestinationInactive, base: ErrFailedPrecondition, reason: ReasonRouteDestinationInactive},
|
||||
{name: "destination currency mismatch", create: RouteDestinationCurrencyMismatch, code: ErrRouteDestinationCurrencyMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationCurrencyMismatch},
|
||||
{name: "destination organization mismatch", create: RouteDestinationOrgMismatch, code: ErrRouteDestinationOrgMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationOrgMismatch},
|
||||
{name: "destination ambiguous", create: RouteDestinationAmbiguous, code: ErrRouteDestinationAmbiguous, base: ErrFailedPrecondition, reason: ReasonRouteDestinationAmbiguous},
|
||||
{name: "destination owner mismatch", create: RouteDestinationOwnerMismatch, code: ErrRouteDestinationOwnerMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationOwnerMismatch},
|
||||
{name: "active source conflict", create: RouteActiveSourceConflict, code: ErrRouteActiveSourceConflict, base: ErrDataConflict, reason: ReasonRouteActiveSourceConflict},
|
||||
{name: "destination type mismatch", create: RouteDestinationTypeMismatch, code: ErrRouteDestinationTypeMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationTypeMismatch},
|
||||
{name: "destination role mismatch", create: RouteDestinationRoleMismatch, code: ErrRouteDestinationRoleMismatch, base: ErrFailedPrecondition, reason: ReasonRouteDestinationRoleMismatch},
|
||||
{name: "idempotency conflict", create: RouteIdempotencyConflict, code: ErrRouteIdempotencyConflict, base: ErrDataConflict, reason: ReasonRouteIdempotencyConflict},
|
||||
{name: "version conflict", create: RouteVersionConflict, code: ErrRouteVersionConflict, base: ErrDataConflict, reason: ReasonRouteVersionConflict},
|
||||
{name: "invalid state", create: RouteInvalidState, code: ErrRouteInvalidState, base: ErrFailedPrecondition, reason: ReasonRouteInvalidState},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
err := tt.create("route error")
|
||||
require.Equal(t, "route error", err.Error())
|
||||
reason, ok := Reason(err)
|
||||
require.True(t, ok)
|
||||
require.Equal(t, tt.reason, reason)
|
||||
require.True(t, errors.Is(err, tt.code))
|
||||
require.True(t, errors.Is(err, tt.base))
|
||||
|
||||
restored, ok := FromReason(reason, "route error")
|
||||
require.True(t, ok)
|
||||
require.True(t, errors.Is(restored, tt.code))
|
||||
require.True(t, errors.Is(restored, tt.base))
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,78 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
)
|
||||
|
||||
const (
|
||||
SignupEmailPolicyCode = "signup_email_rejected"
|
||||
|
||||
SignupEmailPolicyReasonInvalidEmail = "invalid_email"
|
||||
SignupEmailPolicyReasonDisposableDomain = "disposable_domain"
|
||||
SignupEmailPolicyReasonFreeDomain = "free_domain"
|
||||
)
|
||||
|
||||
var ErrSignupEmailPolicyRejected = Define(SignupEmailPolicyCode)
|
||||
|
||||
type SignupEmailPolicyError struct {
|
||||
reason string
|
||||
domain string
|
||||
}
|
||||
|
||||
func NormalizeSignupEmailPolicyReason(reason string) string {
|
||||
switch strings.TrimSpace(reason) {
|
||||
case SignupEmailPolicyReasonInvalidEmail:
|
||||
return SignupEmailPolicyReasonInvalidEmail
|
||||
case SignupEmailPolicyReasonDisposableDomain:
|
||||
return SignupEmailPolicyReasonDisposableDomain
|
||||
case SignupEmailPolicyReasonFreeDomain:
|
||||
return SignupEmailPolicyReasonFreeDomain
|
||||
default:
|
||||
return SignupEmailPolicyReasonInvalidEmail
|
||||
}
|
||||
}
|
||||
|
||||
func IsSignupEmailPolicyReason(reason string) bool {
|
||||
return strings.TrimSpace(reason) == NormalizeSignupEmailPolicyReason(reason)
|
||||
}
|
||||
|
||||
func (e *SignupEmailPolicyError) Error() string {
|
||||
if e == nil || e.reason == "" {
|
||||
return SignupEmailPolicyCode
|
||||
}
|
||||
return SignupEmailPolicyCode + ": " + e.reason
|
||||
}
|
||||
|
||||
func (e *SignupEmailPolicyError) Unwrap() error {
|
||||
return ErrSignupEmailPolicyRejected
|
||||
}
|
||||
|
||||
func (e *SignupEmailPolicyError) Reason() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(e.reason)
|
||||
}
|
||||
|
||||
func (e *SignupEmailPolicyError) Domain() string {
|
||||
if e == nil {
|
||||
return ""
|
||||
}
|
||||
return strings.TrimSpace(e.domain)
|
||||
}
|
||||
|
||||
func SignupEmailPolicyRejected(reason, domain string) error {
|
||||
return &SignupEmailPolicyError{
|
||||
reason: NormalizeSignupEmailPolicyReason(reason),
|
||||
domain: strings.TrimSpace(domain),
|
||||
}
|
||||
}
|
||||
|
||||
func AsSignupEmailPolicyError(err error) (*SignupEmailPolicyError, bool) {
|
||||
var policyErr *SignupEmailPolicyError
|
||||
if errors.As(err, &policyErr) && policyErr != nil {
|
||||
return policyErr, true
|
||||
}
|
||||
return nil, false
|
||||
}
|
||||
@@ -0,0 +1,52 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestSignupEmailPolicyError(t *testing.T) {
|
||||
err := SignupEmailPolicyRejected(SignupEmailPolicyReasonFreeDomain, "gmail.com")
|
||||
policyErr, ok := AsSignupEmailPolicyError(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected signup email policy error")
|
||||
}
|
||||
if policyErr.Reason() != SignupEmailPolicyReasonFreeDomain {
|
||||
t.Fatalf("reason mismatch: got=%q", policyErr.Reason())
|
||||
}
|
||||
if policyErr.Domain() != "gmail.com" {
|
||||
t.Fatalf("domain mismatch: got=%q", policyErr.Domain())
|
||||
}
|
||||
if !errors.Is(err, ErrSignupEmailPolicyRejected) {
|
||||
t.Fatalf("expected signup email policy sentinel")
|
||||
}
|
||||
|
||||
reason, ok := Reason(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected generic reason")
|
||||
}
|
||||
if reason != SignupEmailPolicyReasonFreeDomain {
|
||||
t.Fatalf("generic reason mismatch: got=%q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
func TestSignupEmailPolicyReasonNormalization(t *testing.T) {
|
||||
for _, reason := range []string{
|
||||
SignupEmailPolicyReasonInvalidEmail,
|
||||
SignupEmailPolicyReasonDisposableDomain,
|
||||
SignupEmailPolicyReasonFreeDomain,
|
||||
} {
|
||||
if !IsSignupEmailPolicyReason(reason) {
|
||||
t.Fatalf("expected supported reason: %q", reason)
|
||||
}
|
||||
}
|
||||
|
||||
err := SignupEmailPolicyRejected("unexpected_reason", "")
|
||||
policyErr, ok := AsSignupEmailPolicyError(err)
|
||||
if !ok {
|
||||
t.Fatalf("expected signup email policy error")
|
||||
}
|
||||
if policyErr.Reason() != SignupEmailPolicyReasonInvalidEmail {
|
||||
t.Fatalf("unexpected reasons must not leak: got=%q", policyErr.Reason())
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// ThrottleDetails describes a temporary request limit without coupling callers
|
||||
// to a transport-specific response format.
|
||||
type ThrottleDetails struct {
|
||||
ScopeKind string
|
||||
RetryAfter time.Duration
|
||||
}
|
||||
|
||||
type throttleDetailsError struct {
|
||||
base error
|
||||
msg string
|
||||
details ThrottleDetails
|
||||
}
|
||||
|
||||
func (e throttleDetailsError) Error() string {
|
||||
return Wrap(e.base, e.msg).Error()
|
||||
}
|
||||
|
||||
func (e throttleDetailsError) Unwrap() error {
|
||||
return e.base
|
||||
}
|
||||
|
||||
// WithThrottle annotates an error with retry information while preserving its
|
||||
// classification for errors.Is and errors.As.
|
||||
func WithThrottle(base error, scopeKind string, retryAfter time.Duration, msg string) error {
|
||||
return throttleDetailsError{
|
||||
base: base,
|
||||
msg: msg,
|
||||
details: ThrottleDetails{
|
||||
ScopeKind: strings.TrimSpace(scopeKind),
|
||||
RetryAfter: retryAfter,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// ThrottleDetailsFrom returns retry information attached by WithThrottle.
|
||||
func ThrottleDetailsFrom(err error) (ThrottleDetails, bool) {
|
||||
var throttled throttleDetailsError
|
||||
if !errors.As(err, &throttled) {
|
||||
return ThrottleDetails{}, false
|
||||
}
|
||||
return throttled.details, true
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
package merrors
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestWithThrottlePreservesClassificationAndDetails(t *testing.T) {
|
||||
base := Define("verificationLimited")
|
||||
err := Wrap(
|
||||
WithThrottle(base, " destination ", 45*time.Second, "request is temporarily limited"),
|
||||
"verification failed",
|
||||
)
|
||||
|
||||
if !errors.Is(err, base) {
|
||||
t.Fatalf("throttle error did not preserve classification: %v", err)
|
||||
}
|
||||
details, ok := ThrottleDetailsFrom(err)
|
||||
if !ok {
|
||||
t.Fatal("throttle details were not available through wrapping")
|
||||
}
|
||||
if details.ScopeKind != "destination" || details.RetryAfter != 45*time.Second {
|
||||
t.Fatalf("unexpected throttle details: %+v", details)
|
||||
}
|
||||
if got, want := err.Error(), "verificationLimited: request is temporarily limited: verification failed"; got != want {
|
||||
t.Fatalf("unexpected throttle error text: got=%q want=%q", got, want)
|
||||
}
|
||||
}
|
||||
|
||||
func TestThrottleDetailsFromRejectsUnannotatedError(t *testing.T) {
|
||||
if details, ok := ThrottleDetailsFrom(ServiceUnavailable("dependency unavailable")); ok {
|
||||
t.Fatalf("unexpected throttle details: %+v", details)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
package merrors
|
||||
|
||||
var ErrTreasuryInvalidArgument = Define("treasury.monitoring.invalid_argument")
|
||||
|
||||
var ErrTreasuryConfiguration = Define("treasury.monitoring.configuration_error")
|
||||
|
||||
var ErrTreasuryNotFound = Define("treasury.monitoring.not_found")
|
||||
|
||||
var ErrTreasuryOutOfOrder = Define("treasury.monitoring.out_of_order")
|
||||
|
||||
var ErrTreasuryNegativeBalance = Define("treasury.monitoring.negative_balance")
|
||||
|
||||
var ErrTreasuryAlreadyReversed = Define("treasury.monitoring.movement_already_reversed")
|
||||
|
||||
var ErrTreasuryProjectionMismatch = Define("treasury.monitoring.projection_mismatch")
|
||||
Reference in new issue
Block a user