88 lines
3.2 KiB
Go
88 lines
3.2 KiB
Go
package sendico
|
|
|
|
import (
|
|
"context"
|
|
"net/http"
|
|
|
|
"git.sendico.io/tech/sxchnge/product/payments"
|
|
merrors "git.sendico.io/tech/sxchnge/shared/errors"
|
|
)
|
|
|
|
func (c *Client) validCustomerAccount(a accountDTO, subject string) bool {
|
|
return validRef(a.ID) && a.ScopeRef == c.config.OrganizationID && a.Owner == "" &&
|
|
a.Type == "ACCOUNT_TYPE_ASSET" && a.LedgerRole == "ACCOUNT_ROLE_OPERATING" &&
|
|
a.Status == "ACCOUNT_STATUS_ACTIVE" && !a.AllowNegative && a.Metadata["sxchnge_subject_ref"] == subject &&
|
|
(a.Metadata["external_subject_ref"] == "" || a.Metadata["external_subject_ref"] == subject)
|
|
}
|
|
|
|
// EnsureAccounts also repairs older customers, using provider-enforced
|
|
// organization/subject/currency uniqueness for safe retries after lost responses.
|
|
func (c *Client) EnsureAccounts(ctx context.Context, subject string) error {
|
|
if !validRef(subject) {
|
|
return payments.Unavailable
|
|
}
|
|
root := "/ledger_accounts/" + c.config.OrganizationID
|
|
var data accountListDTO
|
|
if err := c.paymentRequest(ctx, http.MethodGet, root, nil, &data); err != nil {
|
|
return err
|
|
}
|
|
existing := map[string]bool{}
|
|
reconcile := map[string]bool{}
|
|
for _, a := range data.Accounts {
|
|
if a.Metadata["sxchnge_subject_ref"] != subject && a.Metadata["external_subject_ref"] != subject {
|
|
continue
|
|
}
|
|
if !c.validCustomerAccount(a, subject) || existing[a.Currency] {
|
|
return payments.Unavailable
|
|
}
|
|
existing[a.Currency] = true
|
|
reconcile[a.Currency] = a.Metadata["external_subject_ref"] == subject
|
|
}
|
|
missing := []string{}
|
|
for _, code := range []string{"RUB", "USDT", "USDC"} {
|
|
if !existing[code] || reconcile[code] {
|
|
missing = append(missing, code)
|
|
}
|
|
}
|
|
if len(missing) == 0 {
|
|
return nil
|
|
}
|
|
// Older providers can accept the metadata but cannot prevent duplicates.
|
|
// Require the explicit capability before issuing the first create.
|
|
if !data.ExternalSubjectAccounts {
|
|
return payments.Unavailable
|
|
}
|
|
var catalog struct {
|
|
Rows []currencyDTO `json:"currencies"`
|
|
}
|
|
if err := c.paymentRequest(ctx, http.MethodGet, "/payments/fx/currencies/"+c.config.OrganizationID, nil, &catalog); err != nil {
|
|
return err
|
|
}
|
|
for _, code := range missing {
|
|
found := false
|
|
for _, currency := range catalog.Rows {
|
|
if currency.Code == code && currency.Scale != nil && *currency.Scale >= 0 && *currency.Scale <= 8 {
|
|
found = true
|
|
}
|
|
}
|
|
if !found {
|
|
return merrors.OpaqueWrap(payments.Unavailable, payments.Unsupported, "default currency missing from provider catalog")
|
|
}
|
|
}
|
|
// Replaying a bound create also resumes Sendico's durable wallet/route setup
|
|
// if a previous response failed after the ledger itself had been committed.
|
|
for _, code := range missing {
|
|
var response struct {
|
|
Account accountDTO `json:"account"`
|
|
}
|
|
request := createAccountDTO{Currency: code, Type: "asset", LedgerRole: "operating", Metadata: map[string]string{"external_subject_ref": subject, "sxchnge_subject_ref": subject}}
|
|
if err := c.paymentRequest(ctx, http.MethodPost, root, request, &response, http.StatusCreated); err != nil {
|
|
return err
|
|
}
|
|
if !c.validCustomerAccount(response.Account, subject) || response.Account.Currency != code || response.Account.Metadata["external_subject_ref"] != subject {
|
|
return payments.Unavailable
|
|
}
|
|
}
|
|
return nil
|
|
}
|