diff --git a/api/server/interface/api/srequest/ledger.go b/api/server/interface/api/srequest/ledger.go new file mode 100644 index 0000000..1270a50 --- /dev/null +++ b/api/server/interface/api/srequest/ledger.go @@ -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 +} diff --git a/api/server/interface/api/sresponse/ledger.go b/api/server/interface/api/sresponse/ledger.go index 84bcdee..fc75008 100644 --- a/api/server/interface/api/sresponse/ledger.go +++ b/api/server/interface/api/sresponse/ledger.go @@ -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), diff --git a/api/server/internal/server/ledgerapiimp/create.go b/api/server/internal/server/ledgerapiimp/create.go new file mode 100644 index 0000000..8e4ab6d --- /dev/null +++ b/api/server/internal/server/ledgerapiimp/create.go @@ -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") + } +} diff --git a/api/server/internal/server/ledgerapiimp/service.go b/api/server/internal/server/ledgerapiimp/service.go index d9dd121..4114c04 100644 --- a/api/server/internal/server/ledgerapiimp/service.go +++ b/api/server/internal/server/ledgerapiimp/service.go @@ -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