document generation for ops

This commit is contained in:
Stephan D
2026-03-04 15:41:28 +01:00
parent 80b25a8608
commit 92a6191014
21 changed files with 1077 additions and 255 deletions

View File

@@ -12,6 +12,8 @@ import (
"github.com/tech/sendico/billing/documents/storage/model"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
"go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
)
type stubRepo struct {
@@ -94,9 +96,7 @@ func (s *stubTemplate) Render(_ model.ActSnapshot) ([]renderer.Block, error) {
return s.blocks, nil
}
func TestGetDocument_IdempotentAndHashed(t *testing.T) {
ctx := context.Background()
func TestGenerateActPDF_IdempotentAndHashed(t *testing.T) {
snapshot := model.ActSnapshot{
PaymentID: "PAY-123",
Date: time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC),
@@ -105,14 +105,6 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
Currency: "USD",
}
record := &model.DocumentRecord{
PaymentRef: "PAY-123",
Snapshot: snapshot,
}
documentsStore := &stubDocumentsStore{record: record}
repo := &stubRepo{store: documentsStore}
store := newMemDocStore()
tmpl := &stubTemplate{
blocks: []renderer.Block{
{Tag: renderer.TagTitle, Lines: []string{"ACT"}},
@@ -127,62 +119,47 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
},
}
svc := NewService(zap.NewNop(), repo, nil,
svc := NewService(zap.NewNop(), nil, nil,
WithConfig(cfg),
WithDocumentStore(store),
WithTemplateRenderer(tmpl),
)
resp1, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{
PaymentRef: "PAY-123",
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
})
pdf1, hash1, err := svc.generateActPDF(snapshot)
if err != nil {
t.Fatalf("GetDocument first call: %v", err)
t.Fatalf("generateActPDF first call: %v", err)
}
if len(resp1.GetContent()) == 0 {
if len(pdf1) == 0 {
t.Fatalf("expected content on first call")
}
stored := record.Hashes[model.DocumentTypeAct]
if stored == "" {
t.Fatalf("expected stored hash")
if hash1 == "" {
t.Fatalf("expected non-empty hash on first call")
}
footerHash := extractFooterHash(resp1.GetContent())
footerHash := extractFooterHash(pdf1)
if footerHash == "" {
t.Fatalf("expected footer hash in PDF")
}
if stored != footerHash {
t.Fatalf("stored hash mismatch: got %s", stored)
if hash1 != footerHash {
t.Fatalf("stored hash mismatch: got %s", hash1)
}
resp2, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{
PaymentRef: "PAY-123",
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
})
pdf2, hash2, err := svc.generateActPDF(snapshot)
if err != nil {
t.Fatalf("GetDocument second call: %v", err)
t.Fatalf("generateActPDF second call: %v", err)
}
if !bytes.Equal(resp1.GetContent(), resp2.GetContent()) {
t.Fatalf("expected identical PDF bytes on second call")
if hash2 == "" {
t.Fatalf("expected non-empty hash on second call")
}
if tmpl.calls != 1 {
t.Fatalf("expected template to be rendered once, got %d", tmpl.calls)
footerHash2 := extractFooterHash(pdf2)
if footerHash2 == "" {
t.Fatalf("expected footer hash in second PDF")
}
if store.saveCount != 1 {
t.Fatalf("expected document save once, got %d", store.saveCount)
}
if store.loadCount == 0 {
t.Fatalf("expected document load on second call")
if footerHash2 != hash2 {
t.Fatalf("second hash mismatch: got=%s want=%s", footerHash2, hash2)
}
}
@@ -212,3 +189,48 @@ func extractFooterHash(pdf []byte) string {
func isHexDigit(b byte) bool {
return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F')
}
func TestGetOperationDocument_GeneratesPDF(t *testing.T) {
svc := NewService(zap.NewNop(), nil, nil, WithConfig(Config{
Issuer: renderer.Issuer{
LegalName: "Sendico Ltd",
},
}))
resp, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
OrganizationRef: "org-1",
GatewayService: "chain_gateway",
OperationRef: "pay-1:step-1",
PaymentRef: "pay-1",
OperationCode: "crypto.transfer",
OperationLabel: "Outbound transfer",
OperationState: "completed",
Amount: "100.50",
Currency: "USDT",
StartedAtUnixMs: time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC).UnixMilli(),
})
if err != nil {
t.Fatalf("GetOperationDocument failed: %v", err)
}
if len(resp.GetContent()) == 0 {
t.Fatalf("expected non-empty PDF content")
}
if got, want := resp.GetMimeType(), "application/pdf"; got != want {
t.Fatalf("mime_type mismatch: got=%q want=%q", got, want)
}
if got, want := resp.GetFilename(), "operation_pay-1_step-1.pdf"; got != want {
t.Fatalf("filename mismatch: got=%q want=%q", got, want)
}
}
func TestGetOperationDocument_RequiresOperationRef(t *testing.T) {
svc := NewService(zap.NewNop(), nil, nil)
_, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
OrganizationRef: "org-1",
GatewayService: "chain_gateway",
})
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument, got=%v err=%v", status.Code(err), err)
}
}