feat(gl): expose ledger application and grpc operations
This commit is contained in:
@@ -4,21 +4,27 @@ import (
|
||||
"context"
|
||||
|
||||
"gl/application/health"
|
||||
applicationledger "gl/application/ledger"
|
||||
basev1 "gl/gen/base/v1"
|
||||
ledgerv1 "gl/gen/ledger/v1"
|
||||
)
|
||||
|
||||
type HealthHandler struct {
|
||||
type Handler struct {
|
||||
ledgerv1.UnimplementedGeneralLedgerServiceServer
|
||||
service *health.Service
|
||||
health *health.Service
|
||||
ledger *applicationledger.Service
|
||||
}
|
||||
|
||||
func NewHealthHandler(service *health.Service) *HealthHandler {
|
||||
return &HealthHandler{service: service}
|
||||
func NewHandler(healthService *health.Service, ledgerService *applicationledger.Service) *Handler {
|
||||
return &Handler{health: healthService, ledger: ledgerService}
|
||||
}
|
||||
|
||||
func (h *HealthHandler) Health(ctx context.Context, _ *basev1.Empty) (*ledgerv1.HealthResponse, error) {
|
||||
result := h.service.Check(ctx)
|
||||
func NewHealthHandler(service *health.Service) *Handler {
|
||||
return NewHandler(service, nil)
|
||||
}
|
||||
|
||||
func (h *Handler) Health(ctx context.Context, _ *basev1.Empty) (*ledgerv1.HealthResponse, error) {
|
||||
result := h.health.Check(ctx)
|
||||
return &ledgerv1.HealthResponse{
|
||||
Serving: result.Serving,
|
||||
DatabaseReady: result.DatabaseReady,
|
||||
|
||||
@@ -0,0 +1,408 @@
|
||||
package grpcadapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
applicationledger "gl/application/ledger"
|
||||
domain "gl/domain/ledger"
|
||||
ledgerv1 "gl/gen/ledger/v1"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
func (h *Handler) AppendJournal(ctx context.Context, request *ledgerv1.AppendJournalRequest) (*ledgerv1.AppendJournalResponse, error) {
|
||||
command, err := appendJournalCommand(request)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
result, err := h.ledger.AppendJournal(ctx, command)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
return &ledgerv1.AppendJournalResponse{
|
||||
Journal: journalMessage(result.Journal),
|
||||
AlreadyExisted: result.AlreadyExisted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) AppendTransactionEvent(ctx context.Context, request *ledgerv1.AppendTransactionEventRequest) (*ledgerv1.AppendTransactionEventResponse, error) {
|
||||
occurredAt, err := requiredTime(request.GetOccurredAt(), "occurred_at")
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
event, alreadyExisted, err := h.ledger.AppendEvent(ctx, applicationledger.AppendEventCommand{
|
||||
SourceService: request.GetSourceService(),
|
||||
IdempotencyKey: request.GetIdempotencyKey(),
|
||||
SourceTransactionID: request.GetSourceTransactionId(),
|
||||
TrackingCode: request.GetTrackingCode(),
|
||||
EventVersion: request.GetEventVersion(),
|
||||
State: transactionState(request.GetState()),
|
||||
ErrorCode: request.GetErrorCode(),
|
||||
ErrorMessage: request.GetErrorMessage(),
|
||||
OccurredAt: occurredAt,
|
||||
CorrelationID: request.GetCorrelationId(),
|
||||
ActorID: request.GetActorId(),
|
||||
Blockchain: blockchainReference(request.GetBlockchain()),
|
||||
Metadata: request.GetMetadata(),
|
||||
})
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
return &ledgerv1.AppendTransactionEventResponse{
|
||||
Event: eventMessage(event),
|
||||
AlreadyExisted: alreadyExisted,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) GetJournal(ctx context.Context, request *ledgerv1.GetJournalRequest) (*ledgerv1.Journal, error) {
|
||||
var journalID, idempotencyKey string
|
||||
switch lookup := request.GetLookup().(type) {
|
||||
case *ledgerv1.GetJournalRequest_JournalId:
|
||||
journalID = lookup.JournalId
|
||||
case *ledgerv1.GetJournalRequest_IdempotencyKey:
|
||||
idempotencyKey = lookup.IdempotencyKey
|
||||
}
|
||||
journal, err := h.ledger.GetJournal(ctx, journalID, idempotencyKey)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
return journalMessage(journal), nil
|
||||
}
|
||||
|
||||
func (h *Handler) ListEntries(ctx context.Context, request *ledgerv1.ListEntriesRequest) (*ledgerv1.ListEntriesResponse, error) {
|
||||
command := applicationledger.ListCommand{
|
||||
PageSize: request.GetPageSize(),
|
||||
PageToken: request.GetPageToken(),
|
||||
}
|
||||
if request.Account != nil {
|
||||
account := accountReference(request.Account)
|
||||
command.Account = &account
|
||||
}
|
||||
if request.AssetId != nil {
|
||||
assetID := request.GetAssetId()
|
||||
command.AssetID = &assetID
|
||||
}
|
||||
var err error
|
||||
if request.RecordedFrom != nil {
|
||||
value, parseErr := requiredTime(request.RecordedFrom, "recorded_from")
|
||||
if parseErr != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, parseErr.Error())
|
||||
}
|
||||
command.RecordedFrom = &value
|
||||
}
|
||||
if request.RecordedTo != nil {
|
||||
value, parseErr := requiredTime(request.RecordedTo, "recorded_to")
|
||||
if parseErr != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, parseErr.Error())
|
||||
}
|
||||
command.RecordedTo = &value
|
||||
}
|
||||
journals, nextPageToken, err := h.ledger.List(ctx, command)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
response := &ledgerv1.ListEntriesResponse{
|
||||
Journals: make([]*ledgerv1.Journal, 0, len(journals)),
|
||||
NextPageToken: nextPageToken,
|
||||
}
|
||||
for _, journal := range journals {
|
||||
response.Journals = append(response.Journals, journalMessage(journal))
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func (h *Handler) GetBalance(ctx context.Context, request *ledgerv1.GetBalanceRequest) (*ledgerv1.GetBalanceResponse, error) {
|
||||
if request.Account == nil {
|
||||
return nil, status.Error(codes.InvalidArgument, "account is required")
|
||||
}
|
||||
asOf := time.Now().UTC()
|
||||
if request.AsOf != nil {
|
||||
value, err := requiredTime(request.AsOf, "as_of")
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
asOf = value
|
||||
}
|
||||
account := accountReference(request.Account)
|
||||
balance, err := h.ledger.Balance(ctx, account, asOf)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
return &ledgerv1.GetBalanceResponse{
|
||||
Account: accountMessage(account),
|
||||
Balance: balance.String(),
|
||||
AsOf: timestamppb.New(asOf),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (h *Handler) ReplayJournals(ctx context.Context, request *ledgerv1.ReplayJournalsRequest) (*ledgerv1.ReplayJournalsResponse, error) {
|
||||
commands := make([]applicationledger.AppendJournalCommand, 0, len(request.GetJournals()))
|
||||
for _, journal := range request.GetJournals() {
|
||||
command, err := appendJournalCommand(journal)
|
||||
if err != nil {
|
||||
return nil, status.Error(codes.InvalidArgument, err.Error())
|
||||
}
|
||||
commands = append(commands, command)
|
||||
}
|
||||
results, err := h.ledger.Replay(ctx, commands)
|
||||
if err != nil {
|
||||
return nil, rpcError(err)
|
||||
}
|
||||
response := &ledgerv1.ReplayJournalsResponse{Results: make([]*ledgerv1.ReplayJournalResult, 0, len(results))}
|
||||
for _, result := range results {
|
||||
response.Results = append(response.Results, &ledgerv1.ReplayJournalResult{
|
||||
IdempotencyKey: result.Journal.IdempotencyKey,
|
||||
Journal: journalMessage(result.Journal),
|
||||
AlreadyExisted: result.AlreadyExisted,
|
||||
})
|
||||
}
|
||||
return response, nil
|
||||
}
|
||||
|
||||
func appendJournalCommand(request *ledgerv1.AppendJournalRequest) (applicationledger.AppendJournalCommand, error) {
|
||||
occurredAt, err := requiredTime(request.GetOccurredAt(), "occurred_at")
|
||||
if err != nil {
|
||||
return applicationledger.AppendJournalCommand{}, err
|
||||
}
|
||||
command := applicationledger.AppendJournalCommand{
|
||||
SourceService: request.GetSourceService(),
|
||||
IdempotencyKey: request.GetIdempotencyKey(),
|
||||
SourceTransactionID: request.GetSourceTransactionId(),
|
||||
TrackingCode: request.GetTrackingCode(),
|
||||
EffectKind: request.GetEffectKind(),
|
||||
EventVersion: request.GetEventVersion(),
|
||||
ReversalOfJournalID: request.GetReversalOfJournalId(),
|
||||
OccurredAt: occurredAt,
|
||||
CorrelationID: request.GetCorrelationId(),
|
||||
ActorID: request.GetActorId(),
|
||||
Blockchain: blockchainReference(request.GetBlockchain()),
|
||||
Metadata: request.GetMetadata(),
|
||||
Entries: make([]applicationledger.EntryCommand, 0, len(request.GetEntries())),
|
||||
}
|
||||
for _, entry := range request.GetEntries() {
|
||||
if entry.Account == nil {
|
||||
return applicationledger.AppendJournalCommand{}, errors.New("entry account is required")
|
||||
}
|
||||
command.Entries = append(command.Entries, applicationledger.EntryCommand{
|
||||
LineNumber: entry.GetLineNumber(),
|
||||
Account: accountReference(entry.Account),
|
||||
Amount: entry.GetAmount(),
|
||||
Description: entry.GetDescription(),
|
||||
})
|
||||
}
|
||||
return command, nil
|
||||
}
|
||||
|
||||
func requiredTime(value *timestamppb.Timestamp, field string) (time.Time, error) {
|
||||
if value == nil {
|
||||
return time.Time{}, errors.New(field + " is required")
|
||||
}
|
||||
if err := value.CheckValid(); err != nil {
|
||||
return time.Time{}, errors.New(field + " is invalid: " + err.Error())
|
||||
}
|
||||
return value.AsTime().UTC(), nil
|
||||
}
|
||||
|
||||
func accountReference(value *ledgerv1.AccountReference) domain.AccountReference {
|
||||
return domain.AccountReference{
|
||||
Class: accountClass(value.GetAccountClass()),
|
||||
OwnerType: value.GetOwnerType(),
|
||||
OwnerID: value.GetOwnerId(),
|
||||
AssetID: value.GetAssetId(),
|
||||
}
|
||||
}
|
||||
|
||||
func accountClass(value ledgerv1.AccountClass) domain.AccountClass {
|
||||
switch value {
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_USER_AVAILABLE:
|
||||
return domain.AccountClassUserAvailable
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_USER_FROZEN:
|
||||
return domain.AccountClassUserFrozen
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_EXTERNAL_BLOCKCHAIN:
|
||||
return domain.AccountClassExternalBlockchain
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_TREASURY:
|
||||
return domain.AccountClassTreasury
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_MARKET_CLEARING:
|
||||
return domain.AccountClassMarketClearing
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_IPG_CLEARING:
|
||||
return domain.AccountClassIPGClearing
|
||||
case ledgerv1.AccountClass_ACCOUNT_CLASS_COMMISSION_REVENUE:
|
||||
return domain.AccountClassCommissionRevenue
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func accountClassMessage(value domain.AccountClass) ledgerv1.AccountClass {
|
||||
switch value {
|
||||
case domain.AccountClassUserAvailable:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_USER_AVAILABLE
|
||||
case domain.AccountClassUserFrozen:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_USER_FROZEN
|
||||
case domain.AccountClassExternalBlockchain:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_EXTERNAL_BLOCKCHAIN
|
||||
case domain.AccountClassTreasury:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_TREASURY
|
||||
case domain.AccountClassMarketClearing:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_MARKET_CLEARING
|
||||
case domain.AccountClassIPGClearing:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_IPG_CLEARING
|
||||
case domain.AccountClassCommissionRevenue:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_COMMISSION_REVENUE
|
||||
default:
|
||||
return ledgerv1.AccountClass_ACCOUNT_CLASS_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func transactionState(value ledgerv1.TransactionState) domain.TransactionState {
|
||||
switch value {
|
||||
case ledgerv1.TransactionState_TRANSACTION_STATE_CREATED:
|
||||
return domain.TransactionStateCreated
|
||||
case ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_TRANSACTION:
|
||||
return domain.TransactionStatePendingTransaction
|
||||
case ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_ADMIN:
|
||||
return domain.TransactionStatePendingAdmin
|
||||
case ledgerv1.TransactionState_TRANSACTION_STATE_SUCCESSFUL:
|
||||
return domain.TransactionStateSuccessful
|
||||
case ledgerv1.TransactionState_TRANSACTION_STATE_FAILED:
|
||||
return domain.TransactionStateFailed
|
||||
case ledgerv1.TransactionState_TRANSACTION_STATE_SUSPENDED:
|
||||
return domain.TransactionStateSuspended
|
||||
default:
|
||||
return ""
|
||||
}
|
||||
}
|
||||
|
||||
func transactionStateMessage(value domain.TransactionState) ledgerv1.TransactionState {
|
||||
switch value {
|
||||
case domain.TransactionStateCreated:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_CREATED
|
||||
case domain.TransactionStatePendingTransaction:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_TRANSACTION
|
||||
case domain.TransactionStatePendingAdmin:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_PENDING_ADMIN
|
||||
case domain.TransactionStateSuccessful:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_SUCCESSFUL
|
||||
case domain.TransactionStateFailed:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_FAILED
|
||||
case domain.TransactionStateSuspended:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_SUSPENDED
|
||||
default:
|
||||
return ledgerv1.TransactionState_TRANSACTION_STATE_UNSPECIFIED
|
||||
}
|
||||
}
|
||||
|
||||
func blockchainReference(value *ledgerv1.BlockchainReference) domain.BlockchainReference {
|
||||
if value == nil {
|
||||
return domain.BlockchainReference{}
|
||||
}
|
||||
return domain.BlockchainReference{
|
||||
Network: value.GetNetwork(),
|
||||
TransactionHash: value.GetTransactionHash(),
|
||||
LedgerSequence: value.GetLedgerSequence(),
|
||||
}
|
||||
}
|
||||
|
||||
func journalMessage(value domain.Journal) *ledgerv1.Journal {
|
||||
message := &ledgerv1.Journal{
|
||||
JournalId: value.ID,
|
||||
SourceService: value.SourceService,
|
||||
IdempotencyKey: value.IdempotencyKey,
|
||||
SourceTransactionId: value.SourceTransactionID,
|
||||
TrackingCode: value.TrackingCode,
|
||||
EffectKind: value.EffectKind,
|
||||
EventVersion: value.EventVersion,
|
||||
OccurredAt: timestampMessage(value.OccurredAt),
|
||||
RecordedAt: timestampMessage(value.RecordedAt),
|
||||
CorrelationId: value.CorrelationID,
|
||||
ActorId: value.ActorID,
|
||||
Blockchain: blockchainMessage(value.Blockchain),
|
||||
Metadata: value.Metadata,
|
||||
PayloadHash: value.PayloadHash,
|
||||
Entries: make([]*ledgerv1.JournalEntry, 0, len(value.Entries)),
|
||||
}
|
||||
if value.ReversalOfJournalID != "" {
|
||||
message.ReversalOfJournalId = &value.ReversalOfJournalID
|
||||
}
|
||||
for _, entry := range value.Entries {
|
||||
message.Entries = append(message.Entries, &ledgerv1.JournalEntry{
|
||||
LineNumber: entry.LineNumber,
|
||||
Account: accountMessage(entry.Account),
|
||||
Amount: entry.Amount.String(),
|
||||
Description: entry.Description,
|
||||
})
|
||||
}
|
||||
return message
|
||||
}
|
||||
|
||||
func eventMessage(value domain.TransactionEvent) *ledgerv1.TransactionEvent {
|
||||
return &ledgerv1.TransactionEvent{
|
||||
EventId: value.ID,
|
||||
Event: &ledgerv1.AppendTransactionEventRequest{
|
||||
SourceService: value.SourceService,
|
||||
IdempotencyKey: value.IdempotencyKey,
|
||||
SourceTransactionId: value.SourceTransactionID,
|
||||
TrackingCode: value.TrackingCode,
|
||||
EventVersion: value.EventVersion,
|
||||
State: transactionStateMessage(value.State),
|
||||
ErrorCode: value.ErrorCode,
|
||||
ErrorMessage: value.ErrorMessage,
|
||||
OccurredAt: timestampMessage(value.OccurredAt),
|
||||
CorrelationId: value.CorrelationID,
|
||||
ActorId: value.ActorID,
|
||||
Blockchain: blockchainMessage(value.Blockchain),
|
||||
Metadata: value.Metadata,
|
||||
},
|
||||
RecordedAt: timestampMessage(value.RecordedAt),
|
||||
PayloadHash: value.PayloadHash,
|
||||
}
|
||||
}
|
||||
|
||||
func accountMessage(value domain.AccountReference) *ledgerv1.AccountReference {
|
||||
return &ledgerv1.AccountReference{
|
||||
AccountClass: accountClassMessage(value.Class),
|
||||
OwnerType: value.OwnerType,
|
||||
OwnerId: value.OwnerID,
|
||||
AssetId: value.AssetID,
|
||||
}
|
||||
}
|
||||
|
||||
func blockchainMessage(value domain.BlockchainReference) *ledgerv1.BlockchainReference {
|
||||
return &ledgerv1.BlockchainReference{
|
||||
Network: value.Network,
|
||||
TransactionHash: value.TransactionHash,
|
||||
LedgerSequence: value.LedgerSequence,
|
||||
}
|
||||
}
|
||||
|
||||
func timestampMessage(value time.Time) *timestamppb.Timestamp {
|
||||
if value.IsZero() {
|
||||
return nil
|
||||
}
|
||||
return timestamppb.New(value)
|
||||
}
|
||||
|
||||
func rpcError(err error) error {
|
||||
switch {
|
||||
case errors.Is(err, applicationledger.ErrInvalidArgument):
|
||||
return status.Error(codes.InvalidArgument, err.Error())
|
||||
case errors.Is(err, domain.ErrNotFound):
|
||||
return status.Error(codes.NotFound, err.Error())
|
||||
case errors.Is(err, domain.ErrIdempotencyConflict):
|
||||
return status.Error(codes.AlreadyExists, err.Error())
|
||||
case errors.Is(err, domain.ErrIncompleteJournal):
|
||||
return status.Error(codes.FailedPrecondition, err.Error())
|
||||
case errors.Is(err, domain.ErrAlreadyReversed):
|
||||
return status.Error(codes.FailedPrecondition, err.Error())
|
||||
case errors.Is(err, context.Canceled):
|
||||
return status.Error(codes.Canceled, err.Error())
|
||||
case errors.Is(err, context.DeadlineExceeded):
|
||||
return status.Error(codes.DeadlineExceeded, err.Error())
|
||||
default:
|
||||
return status.Error(codes.Internal, "internal ledger error")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,123 @@
|
||||
package grpcadapter
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
applicationledger "gl/application/ledger"
|
||||
domain "gl/domain/ledger"
|
||||
ledgerv1 "gl/gen/ledger/v1"
|
||||
|
||||
"google.golang.org/grpc/codes"
|
||||
"google.golang.org/grpc/status"
|
||||
"google.golang.org/protobuf/types/known/timestamppb"
|
||||
)
|
||||
|
||||
type ledgerRepositoryStub struct {
|
||||
journal domain.Journal
|
||||
}
|
||||
|
||||
func (r *ledgerRepositoryStub) Append(_ context.Context, journal domain.Journal) (domain.AppendResult, error) {
|
||||
journal.RecordedAt = time.Unix(2, 0).UTC()
|
||||
r.journal = journal
|
||||
return domain.AppendResult{JournalID: journal.ID}, nil
|
||||
}
|
||||
|
||||
func (r *ledgerRepositoryStub) AppendEvent(_ context.Context, event domain.TransactionEvent) (domain.TransactionEvent, bool, error) {
|
||||
event.RecordedAt = time.Unix(2, 0).UTC()
|
||||
return event, false, nil
|
||||
}
|
||||
|
||||
func (r *ledgerRepositoryStub) GetByID(context.Context, string) (domain.Journal, error) {
|
||||
return r.journal, nil
|
||||
}
|
||||
|
||||
func (r *ledgerRepositoryStub) GetByIdempotencyKey(context.Context, string) (domain.Journal, error) {
|
||||
return r.journal, nil
|
||||
}
|
||||
|
||||
func (r *ledgerRepositoryStub) List(context.Context, domain.JournalFilter) ([]domain.Journal, error) {
|
||||
return []domain.Journal{r.journal}, nil
|
||||
}
|
||||
|
||||
func (r *ledgerRepositoryStub) Balance(context.Context, domain.AccountReference, time.Time) (domain.Amount, error) {
|
||||
return domain.ParseAmount("12.5")
|
||||
}
|
||||
|
||||
func TestAppendJournalMapsProtoToExactDomainAndBack(t *testing.T) {
|
||||
repository := &ledgerRepositoryStub{}
|
||||
service := applicationledger.NewService(repository, func() (string, error) {
|
||||
return "11111111-1111-4111-8111-111111111111", nil
|
||||
})
|
||||
handler := NewHandler(nil, service)
|
||||
|
||||
response, err := handler.AppendJournal(context.Background(), &ledgerv1.AppendJournalRequest{
|
||||
SourceService: "wallet",
|
||||
IdempotencyKey: "wallet:1:internal-transfer:v1",
|
||||
SourceTransactionId: "1",
|
||||
EffectKind: "internal-transfer",
|
||||
EventVersion: 1,
|
||||
OccurredAt: timestamppb.New(time.Unix(1, 0).UTC()),
|
||||
Entries: []*ledgerv1.JournalEntry{
|
||||
{
|
||||
LineNumber: 1,
|
||||
Account: &ledgerv1.AccountReference{
|
||||
AccountClass: ledgerv1.AccountClass_ACCOUNT_CLASS_USER_AVAILABLE,
|
||||
OwnerType: "user",
|
||||
OwnerId: "1",
|
||||
AssetId: 5,
|
||||
},
|
||||
Amount: "-1.2500",
|
||||
},
|
||||
{
|
||||
LineNumber: 2,
|
||||
Account: &ledgerv1.AccountReference{
|
||||
AccountClass: ledgerv1.AccountClass_ACCOUNT_CLASS_USER_AVAILABLE,
|
||||
OwnerType: "user",
|
||||
OwnerId: "2",
|
||||
AssetId: 5,
|
||||
},
|
||||
Amount: "1.25",
|
||||
},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if got := response.GetJournal().GetEntries()[0].GetAmount(); got != "-1.25" {
|
||||
t.Fatalf("unexpected canonical amount: %q", got)
|
||||
}
|
||||
if response.GetJournal().GetPayloadHash() == "" {
|
||||
t.Fatal("payload hash is missing")
|
||||
}
|
||||
}
|
||||
|
||||
func TestAppendJournalRejectsMissingTimestamp(t *testing.T) {
|
||||
handler := NewHandler(nil, nil)
|
||||
_, err := handler.AppendJournal(context.Background(), &ledgerv1.AppendJournalRequest{})
|
||||
if status.Code(err) != codes.InvalidArgument {
|
||||
t.Fatalf("expected InvalidArgument, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRPCErrorMapping(t *testing.T) {
|
||||
for _, testCase := range []struct {
|
||||
err error
|
||||
code codes.Code
|
||||
}{
|
||||
{err: applicationledger.ErrInvalidArgument, code: codes.InvalidArgument},
|
||||
{err: domain.ErrNotFound, code: codes.NotFound},
|
||||
{err: domain.ErrIdempotencyConflict, code: codes.AlreadyExists},
|
||||
{err: domain.ErrIncompleteJournal, code: codes.FailedPrecondition},
|
||||
{err: domain.ErrAlreadyReversed, code: codes.FailedPrecondition},
|
||||
{err: context.Canceled, code: codes.Canceled},
|
||||
{err: context.DeadlineExceeded, code: codes.DeadlineExceeded},
|
||||
{err: errors.New("database details must not escape"), code: codes.Internal},
|
||||
} {
|
||||
if got := status.Code(rpcError(testCase.err)); got != testCase.code {
|
||||
t.Fatalf("rpcError(%v) = %v, want %v", testCase.err, got, testCase.code)
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user