+ ledger account open endpoint

This commit is contained in:
Stephan D
2026-01-05 12:57:17 +01:00
parent 9a5c087940
commit 2f34b5a827
4 changed files with 184 additions and 0 deletions

View File

@@ -0,0 +1,48 @@
package srequest
import (
"strings"
"github.com/tech/sendico/pkg/merrors"
)
type LedgerAccountType string
const (
LedgerAccountTypeUnspecified LedgerAccountType = "unspecified"
LedgerAccountTypeAsset LedgerAccountType = "asset"
LedgerAccountTypeLiability LedgerAccountType = "liability"
LedgerAccountTypeRevenue LedgerAccountType = "revenue"
LedgerAccountTypeExpense LedgerAccountType = "expense"
)
type LedgerAccountStatus string
const (
LedgerAccountStatusUnspecified LedgerAccountStatus = "unspecified"
LedgerAccountStatusActive LedgerAccountStatus = "active"
LedgerAccountStatusFrozen LedgerAccountStatus = "frozen"
)
type CreateLedgerAccount struct {
AccountCode string `json:"accountCode"`
AccountType LedgerAccountType `json:"accountType"`
Currency string `json:"currency"`
Status LedgerAccountStatus `json:"status,omitempty"`
AllowNegative bool `json:"allowNegative,omitempty"`
IsSettlement bool `json:"isSettlement,omitempty"`
Metadata map[string]string `json:"metadata,omitempty"`
}
func (r *CreateLedgerAccount) Validate() error {
if strings.TrimSpace(r.AccountCode) == "" {
return merrors.InvalidArgument("accountCode is required", "accountCode")
}
if strings.TrimSpace(r.Currency) == "" {
return merrors.InvalidArgument("currency is required", "currency")
}
if strings.TrimSpace(string(r.AccountType)) == "" || strings.EqualFold(string(r.AccountType), string(LedgerAccountTypeUnspecified)) {
return merrors.InvalidArgument("accountType is required", "accountType")
}
return nil
}

View File

@@ -29,6 +29,11 @@ type ledgerAccountsResponse struct {
Accounts []ledgerAccount `json:"accounts"`
}
type ledgerAccountResponse struct {
authResponse `json:",inline"`
Account ledgerAccount `json:"account"`
}
type ledgerMoney struct {
Amount string `json:"amount"`
Currency string `json:"currency"`
@@ -57,6 +62,13 @@ func LedgerAccounts(logger mlogger.Logger, accounts []*ledgerv1.LedgerAccount, a
})
}
func LedgerAccountCreated(logger mlogger.Logger, account *ledgerv1.LedgerAccount, accessToken *TokenData) http.HandlerFunc {
return response.Created(logger, ledgerAccountResponse{
Account: toLedgerAccount(account),
authResponse: authResponse{AccessToken: *accessToken},
})
}
func LedgerBalance(logger mlogger.Logger, resp *ledgerv1.BalanceResponse, accessToken *TokenData) http.HandlerFunc {
return response.Ok(logger, ledgerBalanceResponse{
Balance: toLedgerBalance(resp),

View File

@@ -0,0 +1,122 @@
package ledgerapiimp
import (
"encoding/json"
"errors"
"net/http"
"strings"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/bson/primitive"
"go.uber.org/zap"
)
func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc {
orgRef, err := a.oph.GetRef(r)
if err != nil {
a.logger.Warn("Failed to parse organization reference for ledger account create", zap.Error(err), zap.String(a.oph.Name(), a.oph.GetID(r)))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
}
ctx := r.Context()
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, primitive.NilObjectID, model.ActionCreate)
if err != nil {
a.logger.Warn("Failed to check ledger accounts access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err)
}
if !allowed {
a.logger.Debug("Access denied when creating ledger account", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "ledger accounts write permission denied")
}
payload, err := decodeLedgerAccountCreatePayload(r)
if err != nil {
a.logger.Warn("Failed to decode ledger account create payload", zap.Error(err), mutil.PLog(a.oph, r))
return response.BadPayload(a.logger, a.Name(), err)
}
accountType, err := mapLedgerAccountType(payload.AccountType)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
status, err := mapLedgerAccountStatus(payload.Status)
if err != nil {
return response.BadPayload(a.logger, a.Name(), err)
}
if a.client == nil {
return response.Internal(a.logger, mservice.Ledger, errors.New("ledger client is not configured"))
}
resp, err := a.client.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
OrganizationRef: orgRef.Hex(),
AccountCode: payload.AccountCode,
AccountType: accountType,
Currency: payload.Currency,
Status: status,
AllowNegative: payload.AllowNegative,
IsSettlement: payload.IsSettlement,
Metadata: payload.Metadata,
})
if err != nil {
a.logger.Warn("Failed to create ledger account", zap.Error(err), zap.String("organization_ref", orgRef.Hex()))
return response.Auto(a.logger, mservice.Ledger, err)
}
return sresponse.LedgerAccountCreated(a.logger, resp.GetAccount(), token)
}
func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) {
defer r.Body.Close()
payload := &srequest.CreateLedgerAccount{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
return nil, merrors.InvalidArgument("invalid payload: " + err.Error())
}
payload.AccountCode = strings.TrimSpace(payload.AccountCode)
payload.Currency = strings.ToUpper(strings.TrimSpace(payload.Currency))
if len(payload.Metadata) == 0 {
payload.Metadata = nil
}
if err := payload.Validate(); err != nil {
return nil, err
}
return payload, nil
}
func mapLedgerAccountType(accountType srequest.LedgerAccountType) (ledgerv1.AccountType, error) {
switch strings.ToUpper(strings.TrimSpace(string(accountType))) {
case "ACCOUNT_TYPE_ASSET", "ASSET":
return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, nil
case "ACCOUNT_TYPE_LIABILITY", "LIABILITY":
return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY, nil
case "ACCOUNT_TYPE_REVENUE", "REVENUE":
return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE, nil
case "ACCOUNT_TYPE_EXPENSE", "EXPENSE":
return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE, nil
case "", "ACCOUNT_TYPE_UNSPECIFIED", "UNSPECIFIED":
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("accountType is required", "accountType")
default:
return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED, merrors.InvalidArgument("unsupported accountType: "+string(accountType), "accountType")
}
}
func mapLedgerAccountStatus(status srequest.LedgerAccountStatus) (ledgerv1.AccountStatus, error) {
switch strings.ToUpper(strings.TrimSpace(string(status))) {
case "", "ACCOUNT_STATUS_UNSPECIFIED", "UNSPECIFIED":
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, nil
case "ACCOUNT_STATUS_ACTIVE", "ACTIVE":
return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, nil
case "ACCOUNT_STATUS_FROZEN", "FROZEN":
return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN, nil
default:
return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED, merrors.InvalidArgument("unsupported status: "+string(status), "status")
}
}

View File

@@ -21,6 +21,7 @@ import (
)
type ledgerClient interface {
CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error)
ListAccounts(ctx context.Context, req *ledgerv1.ListAccountsRequest) (*ledgerv1.ListAccountsResponse, error)
GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error)
Close() error
@@ -75,6 +76,7 @@ func CreateAPI(apiCtx eapi.API) (*LedgerAPI, error) {
}
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listAccounts)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Post, p.createAccount)
apiCtx.Register().AccountHandler(p.Name(), p.aph.AddRef(p.oph.AddRef("/"))+"/balance", api.Get, p.getBalance)
return p, nil