Compare commits

...

171 Commits

Author SHA1 Message Date
f9aea3a7ca Merge pull request 'added missing test file for build to run' (#730) from SEND074 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #730
2026-03-13 14:14:59 +00:00
c188def331 Merge branch 'main' into SEND074 2026-03-13 13:59:30 +00:00
Arseni
5daf076ee4 added missing test file for build to run 2026-03-13 16:58:36 +03:00
12dc4418e0 Merge pull request 'fix for quote and operations addition' (#729) from SEND073 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #729
2026-03-13 13:26:10 +00:00
Arseni
530432e3f8 fix for quote and operations addition 2026-03-13 16:07:22 +03:00
34a7edd50c Merge pull request 'refactor of money utils with new money2 package' (#726) from SEND072 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #726
2026-03-13 10:29:56 +00:00
8c7ac38bdb Merge pull request 'docs format updated' (#728) from doc-725 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
Reviewed-on: #728
2026-03-13 09:00:41 +00:00
Stephan D
d07496bf5f docs format updated 2026-03-13 10:00:19 +01:00
79318ba77b Merge pull request 'docs format updated' (#727) from doc-725 into main
Some checks failed
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #727
2026-03-13 00:29:22 +00:00
Stephan D
f1840690e1 docs format updated 2026-03-13 01:28:51 +01:00
Arseni
0091191d97 refactor of money utils with new money2 package 2026-03-13 03:17:29 +03:00
b4eb1437f6 Merge pull request 'isolated metadata keys constants' (#724) from SEND071 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #724
Reviewed-by: tech <tech.sendico@proton.me>
2026-03-12 12:07:50 +00:00
Arseni
0ff850b1de isolated metadata keys constants 2026-03-12 14:26:54 +03:00
5085a55e44 [infra][rebuild] Merge pull request 'propagated payment comment to bff' (#723) from po-722 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
[infra][rebuild] Reviewed-on: #723
2026-03-11 23:44:09 +00:00
Stephan D
b440df97d5 propagated payment commentto bff 2026-03-12 00:42:49 +01:00
4958bdb500 Merge pull request 'added comment for payment, changed intent and added amount ui in operations' (#719) from SEND069 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #719
Reviewed-by: tech <tech.sendico@proton.me>
2026-03-11 21:59:21 +00:00
Arseni
13b84e1e0f added comment for payment, changed intent and added amount ui in operations 2026-03-12 00:09:38 +03:00
ddc2f1facc Merge pull request 'Orchestrator refactoring + planned amounts' (#718) from po-417 into main
Some checks failed
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_methods Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline failed
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/discovery Pipeline failed
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline failed
ci/woodpecker/push/gateway_chain Pipeline failed
Reviewed-on: #718
2026-03-11 19:07:21 +00:00
Stephan D
f578278205 Orchestrator refactoring + planned amounts 2026-03-11 20:04:10 +01:00
208b4283d0 Merge pull request 'Payout Page + Wallet Card Refactor' (#695) from SEND066 into main
Some checks failed
ci/woodpecker/push/gateway_tgsettle Pipeline is pending
ci/woodpecker/push/gateway_tron Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_methods Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline failed
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/discovery Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline failed
ci/woodpecker/push/fx_oracle Pipeline failed
Reviewed-on: #695
Reviewed-by: tech <tech.sendico@proton.me>
2026-03-11 16:45:31 +00:00
Arseni
0172176978 fixes 2026-03-11 18:26:21 +03:00
73f067c8c2 Merge pull request 'added payment orchestrator optimizer tweaks' (#716) from po-714 into main
Some checks failed
ci/woodpecker/push/payments_orchestrator Pipeline failed
Reviewed-on: #716
2026-03-11 14:58:53 +00:00
Stephan D
9f998b8134 added payment orchestrator optimizer tweaks 2026-03-11 15:58:18 +01:00
27b4ece6c6 Merge pull request 'explicit fee execution' (#715) from po-713 into main
Some checks failed
ci/woodpecker/push/payments_orchestrator Pipeline failed
Reviewed-on: #715
2026-03-11 13:21:23 +00:00
Stephan D
15b03b1bc8 explicit fee execution 2026-03-11 14:20:50 +01:00
fdd8dd8845 Merge branch 'main' into SEND066 2026-03-11 12:07:24 +00:00
Arseni
3b23eada33 Added missing file 2026-03-11 14:59:24 +03:00
b4b9507e7e Merge pull request 'added code generation before testing' (#712) from cicd-709 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #712
2026-03-11 11:04:22 +00:00
Stephan D
6911757e1d added code generation before testing 2026-03-11 12:03:56 +01:00
fba992f898 Merge pull request 'fixed frontend tests' (#711) from cicd-709 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #711
2026-03-11 11:00:57 +00:00
Stephan D
5a8392a6d0 fixed frontend tests 2026-03-11 12:00:11 +01:00
6b82825494 Merge pull request 'fixed frontend tests' (#710) from cicd-709 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #710
2026-03-11 10:57:27 +00:00
Stephan D
54e9821886 fixed frontend tests 2026-03-11 11:56:02 +01:00
c74444ab0b Merge pull request 'extended aurora scenarios + payment operation amounts' (#708) from po-707 into main
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #708
2026-03-11 00:34:23 +00:00
aba63ecd37 Merge pull request 'infra updates' (#702) from SEND068 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #702
Reviewed-by: tech <tech.sendico@proton.me>
2026-03-11 00:34:13 +00:00
Stephan D
9ad2104d7d extended aurora scenarios + payment operation amounts 2026-03-11 01:09:11 +01:00
e446486b77 Merge pull request 'cicd-705' (#706) from cicd-705 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #706
2026-03-10 18:51:00 +00:00
Stephan D
65b04fba39 removed buildx setting 2026-03-10 19:50:34 +01:00
Stephan D
bbdbd9a197 amd64 image binding 2026-03-10 19:49:29 +01:00
Arseni
049b23516a added missing files 2026-03-10 21:45:41 +03:00
3862fa4e52 Merge pull request '+source +destination in payments' (#704) from bff-703 into main
Some checks failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #704
2026-03-10 18:32:38 +00:00
Arseni
7ae4518926 config for vault 2026-03-10 21:26:32 +03:00
Stephan D
e5b4de5d48 +source +destination in payments 2026-03-10 19:15:20 +01:00
Arseni
0e64ab5558 deleted unnecessary changes for this branch 2026-03-10 20:48:24 +03:00
Arseni
840a7f85c8 updated for infra 2026-03-10 20:40:20 +03:00
9c2b3bf8bd Merge pull request 'fixed linting step to CG0 enabled = false' (#701) from cicd-700 into main
Some checks failed
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_methods Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline failed
ci/woodpecker/push/ledger Pipeline failed
ci/woodpecker/push/gateway_tgsettle Pipeline failed
Reviewed-on: #701
2026-03-10 17:26:55 +00:00
Stephan D
41720a6817 fixed linting step to CG0 enabled = false 2026-03-10 18:25:21 +01:00
Stephan D
918dbe8bb5 linting added to CI + bypass tags
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
ci/woodpecker/push/gateway_mntx Pipeline is pending
ci/woodpecker/push/gateway_tron Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/payments_methods Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/gateway_chain Pipeline is pending
ci/woodpecker/push/gateway_tgsettle Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/billing_documents Pipeline failed
ci/woodpecker/push/callbacks Pipeline failed
ci/woodpecker/push/discovery Pipeline failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline failed
2026-03-10 12:39:30 +01:00
Stephan D
6cc0340ba3 version bump 2026-03-10 12:33:07 +01:00
Stephan D
e77d1ab793 linting 2026-03-10 12:31:09 +01:00
d87e709f43 Merge pull request 'removed kaniko caching' (#698) from cicd-697 into main
Reviewed-on: #698
2026-03-06 18:05:42 +00:00
Stephan D
185f8f2ed6 removed kaniko caching 2026-03-06 19:04:36 +01:00
2160b6bf4d Merge pull request 'Billing docs improvement + build opt' (#696) from docs-693 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #696
2026-03-06 15:20:31 +00:00
Stephan D
54bbe41f7a Billing docs improvement + build opt 2026-03-06 16:20:01 +01:00
Arseni
f7f0612af9 Merge remote-tracking branch 'origin/main' into SEND066
merge fresh main
2026-03-06 18:08:26 +03:00
Arseni
0aceb2f441 added missed loc instead of hardcode 2026-03-06 18:05:04 +03:00
Arseni
281b3834d3 wallet card redesign 2026-03-06 17:48:36 +03:00
6633a1d807 Merge pull request 'gw <-> po contracts tests' (#692) from gw-691 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #692
2026-03-06 14:45:46 +00:00
Stephan D
88b279dd78 gw <-> po contracts tests 2026-03-06 15:45:14 +01:00
0f42a0e77f Merge pull request 'Chimera Settle service' (#690) from chsettle-689 into main
Reviewed-on: #690
2026-03-06 14:43:02 +00:00
Stephan D
10bcdb4fe2 Chimera Settle service 2026-03-06 15:42:32 +01:00
Arseni
2b0ada1541 Merge remote-tracking branch 'origin/main' into SEND066
merge fresh main into SEND66
2026-03-06 17:18:39 +03:00
ea5ec79a6e Merge pull request 'fixed po <-> tgsettle contract' (#688) from po-687 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #688
2026-03-06 14:12:46 +00:00
Stephan D
3295b9d9f0 fixed po <-> tgsettle contract 2026-03-06 15:12:14 +01:00
031b8931ca Merge pull request 'fixed tgsettle upsert logic' (#686) from tg-685 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #686
2026-03-06 12:50:56 +00:00
Stephan D
4295456f63 fixed tgsettle upsert logic 2026-03-06 13:50:13 +01:00
2b1b4135f4 Merge pull request 'mntx error codes update' (#684) from mntx-683 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #684
2026-03-06 12:35:09 +00:00
Stephan D
c60e7d2329 mntx error codes update 2026-03-06 12:14:32 +01:00
be49254769 Merge pull request 'bff USDT ledger creation' (#682) from bff-681 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #682
2026-03-06 10:58:27 +00:00
Stephan D
34e507b664 bff USDT ledger creation 2026-03-06 11:58:07 +01:00
Arseni
b67d199427 Merge remote-tracking branch 'origin/main' into SEND066
merge lastest updates in main
2026-03-06 01:14:40 +03:00
b481de9ffc Merge pull request 'New comments section in the requests/responses' (#679) from bff-677 into main
Reviewed-on: #679
2026-03-05 19:29:10 +00:00
Stephan D
0c29e7686d New comments section in the requests/responses 2026-03-05 20:28:28 +01:00
5b26a70a15 Merge pull request 'New comments section in the requests/responses' (#678) from bff-677 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #678
2026-03-05 19:28:05 +00:00
Stephan D
b832c2a7c4 New comments section in the requests/responses 2026-03-05 20:27:45 +01:00
Arseni
97b16542c2 ledger top up functionality and few small fixes for project architechture and design 2026-03-05 21:49:23 +03:00
Arseni
39c04beb21 Merge remote-tracking branch 'origin/main' into SEND066
merge main into SEND066
2026-03-05 21:12:43 +03:00
15393765b9 Merge pull request 'fixes for multiple payout' (#674) from SEND067 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #674
2026-03-05 16:35:37 +00:00
Arseni
440b6a2553 fixes for multiple payout 2026-03-05 19:28:02 +03:00
bc76cfe063 Merge pull request 'tg-670' (#671) from tg-670 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #671
2026-03-05 13:47:37 +00:00
Stephan D
ed8f7c519c moved tg settings to db 2026-03-05 14:46:34 +01:00
Stephan D
71d99338f2 moved tg settings to db 2026-03-05 14:46:26 +01:00
b499778bce Merge pull request 'fixed treasury messages' (#669) from tg-666 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #669
2026-03-05 13:18:13 +00:00
Stephan D
4a554833c4 fixed treasury messages 2026-03-05 14:17:50 +01:00
b7ea11a62b Merge pull request 'fixed treasury messages' (#668) from tg-666 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #668
2026-03-05 13:09:47 +00:00
Stephan D
026f698d9b fixed treasury messages 2026-03-05 14:09:21 +01:00
0da6078468 Merge pull request 'fixed treasury messages' (#667) from tg-666 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #667
2026-03-05 12:59:57 +00:00
Stephan D
3b65a2dc3a fixed treasury messages 2026-03-05 13:59:38 +01:00
Arseni
d6a3a0cc5b solyanka iz fix for payout page design, ledger wallet now clickable 2026-03-05 15:48:52 +03:00
a9b00b6871 Merge pull request 'fixed fee direction' (#665) from po-664 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #665
2026-03-05 12:27:19 +00:00
d64ad89072 Merge pull request 'missing asset in billing' (#663) from SEND065 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
Reviewed-on: #663
2026-03-05 12:25:53 +00:00
Stephan D
4a5e26b03a fixed fee direction 2026-03-05 13:24:41 +01:00
Arseni
d61eee99bc missing asset in billing 2026-03-05 15:02:52 +03:00
1e376da719 Merge pull request 'fixed icon path in billing' (#659) from SEND064 into main
Some checks failed
ci/woodpecker/push/gateway_chain Pipeline is pending
ci/woodpecker/push/gateway_mntx Pipeline is pending
ci/woodpecker/push/gateway_tgsettle Pipeline is pending
ci/woodpecker/push/gateway_tron Pipeline is pending
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_methods Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/billing_documents Pipeline failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #659
2026-03-05 11:35:37 +00:00
a8b0c70b65 Merge pull request 'fixed succcess operation matching' (#661) from po-660 into main
All checks were successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #661
2026-03-05 11:24:54 +00:00
Stephan D
8981d296c8 fixed succcess operation matching 2026-03-05 12:23:58 +01:00
Arseni
7e5a98acd7 fixed icon path in billing 2026-03-05 13:59:17 +03:00
8577239dd6 Merge pull request 'improved tgsettle messages + storage fixes' (#658) from tg-657 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #658
2026-03-05 10:54:28 +00:00
Stephan D
5e59fea7e5 improved tgsettle messages + storage fixes 2026-03-05 11:54:07 +01:00
801f349aa8 Merge pull request 'Fixed bot verbosity' (#656) from tg-655 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #656
2026-03-05 10:02:54 +00:00
Stephan D
d1e47841cc Fixed bot verbosity 2026-03-05 11:02:30 +01:00
364731a8c7 Merge pull request 'added download for operation and included fixes for source of payments' (#639) from SEND063 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #639
Reviewed-by: tech <tech.sendico@proton.me>
2026-03-05 08:29:45 +00:00
Arseni
519a2b1304 few fixes and made sure ledger widget displays the name of ledger wallet 2026-03-05 01:48:53 +03:00
d027f2deda Merge pull request 'Fixed po sending comission' (#648) from po-647 into main
All checks were successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #648
2026-03-04 22:22:02 +00:00
Stephan D
ba5a3312b5 Fixed po sending comission 2026-03-04 23:21:35 +01:00
f2c9685eb1 Merge pull request '/start command' (#646) from tg-643 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #646
2026-03-04 22:01:45 +00:00
Stephan D
e80cb3eed1 /start command 2026-03-04 23:01:21 +01:00
5f647904d7 Merge pull request 'Treasury bot + ledger fix' (#644) from tg-643 into main
All checks were successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #644
2026-03-04 19:02:21 +00:00
Stephan D
b6f05f52dc Treasury bot + ledger fix 2026-03-04 20:01:37 +01:00
75555520f3 Merge pull request 'fixed ledger account name propagation when creating ledger account' (#642) from ledger-614 into main
All checks were successful
ci/woodpecker/push/ledger Pipeline was successful
Reviewed-on: #642
2026-03-04 17:53:00 +00:00
Stephan D
d666c4ce51 fixed ledger account name propagation when creating ledger account 2026-03-04 18:52:43 +01:00
706a57e860 Merge pull request 'op payment info added' (#641) from bff-640 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #641
2026-03-04 17:03:11 +00:00
Stephan D
f7b0915303 op payment info added 2026-03-04 18:02:36 +01:00
Arseni
c59538869b updated document upload according to fresh api 2026-03-04 18:07:08 +03:00
Arseni
aff804ec58 SEND063 2026-03-04 17:43:18 +03:00
2bab8371b8 Merge pull request 'billing-637' (#638) from billing-637 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #638
2026-03-04 14:42:40 +00:00
Stephan D
af8ab8238e removeod obsolete file 2026-03-04 15:41:56 +01:00
Stephan D
92a6191014 document generation for ops 2026-03-04 15:41:28 +01:00
80b25a8608 Merge pull request 'added gateway and operation references' (#635) from bff-634 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #635
2026-03-04 12:55:46 +00:00
17d954c689 Merge pull request 'removed payments polling' (#633) from SEND062 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #633
2026-03-04 12:55:35 +00:00
Stephan D
349e8afdc5 fixed operation ref description 2026-03-04 13:54:56 +01:00
Stephan D
8a1e44c038 removed strict mode from mntx 2026-03-04 13:52:56 +01:00
Stephan D
3fcbbfb08a added gateway and operation references 2026-03-04 13:51:48 +01:00
Arseni
75f3678b90 removed payments polling 2026-03-04 15:34:16 +03:00
4fa641f971 Merge pull request 'serial payouts' (#632) from mntx-627 into main
Some checks failed
ci/woodpecker/push/gateway_mntx Pipeline failed
Reviewed-on: #632
2026-03-04 09:33:07 +00:00
Stephan D
eb8b7b3402 serial payouts 2026-03-04 10:32:37 +01:00
3a8935f5f0 Merge pull request 'fixed rescheduling supporting callback error code processing' (#631) from mntx-627 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #631
2026-03-04 08:19:12 +00:00
Stephan D
d92be5eedc fixed rescheduling supporting callback error code processing 2026-03-04 09:18:15 +01:00
94406373e6 Merge pull request 'fixed quotation currency inference' (#630) from pq-626 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #630
2026-03-04 04:13:55 +00:00
eda5bf19ad Merge pull request 'mntx throtling' (#629) from mntx-627 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #629
2026-03-04 04:04:53 +00:00
Stephan D
5629f5fcb2 mntx throtling 2026-03-04 05:04:34 +01:00
95f7698661 Merge pull request 'mntx gateway throttling' (#628) from mntx-627 into main
Some checks failed
ci/woodpecker/push/gateway_mntx Pipeline failed
Reviewed-on: #628
2026-03-04 04:03:32 +00:00
Stephan D
4e70873a94 mntx gateway throttling 2026-03-04 05:02:52 +01:00
Stephan D
de07b9a792 fixed quotation currency inference 2026-03-04 04:50:31 +01:00
9b794a3065 Merge pull request 'mntx-624' (#625) from mntx-624 into main
Some checks failed
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #625
2026-03-04 01:47:43 +00:00
Stephan D
56bf49aa03 fixed mntx payout sequence 2026-03-04 02:46:51 +01:00
Stephan D
8377b6b2af fixed operations idempotency 2026-03-04 02:27:12 +01:00
f06208348b Merge pull request 'fixed default to grpcs' (#623) from tron-622 into main
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #623
2026-03-04 00:31:21 +00:00
Stephan D
b4c09cfb3b fixed default to grpcs 2026-03-04 01:30:56 +01:00
00812fa2bd Merge pull request 'refactored deprecated code' (#621) from pkg-620 into main
Some checks failed
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #621
2026-03-03 21:29:22 +00:00
Stephan D
ce5f90939f refactored deprecated code 2026-03-03 22:29:03 +01:00
1b40b173eb Merge pull request 'added ledger as source of funds for payouts' (#618) from SEND061 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #618
2026-03-03 21:26:45 +00:00
cd8e8071a9 Merge pull request 'fixed address resolution' (#619) from tron-616 into main
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #619
2026-03-03 18:37:32 +00:00
Stephan D
f7a1027de7 fixed address resolution 2026-03-03 19:37:09 +01:00
c5b3dfbd7a Merge pull request 'fixed address resolution' (#617) from tron-616 into main
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #617
2026-03-03 18:13:58 +00:00
Stephan D
41cb826d26 fixed address resolution 2026-03-03 19:13:36 +01:00
Arseni
51c72a87ae added ledger as souec of funds for payouts 2026-03-03 21:03:30 +03:00
Stephan D
3f578353da fixed tron ip connection
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
2026-03-03 18:32:26 +01:00
7cac494509 Merge pull request 'Fixed tron driver settings' (#613) from tron-612 into main
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #613
2026-03-03 16:51:11 +00:00
Stephan D
d8f0febc5e Fixed tron driver settings 2026-03-03 17:50:50 +01:00
34a8a5d057 Fixed mntx discovery
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Fixed mntx discovery
2026-03-03 12:16:46 +00:00
Stephan D
83745bcd10 fixed mntx discovery 2026-03-03 13:15:42 +01:00
Stephan D
f9acb47ad7 mntx gateway lookup name fixed 2026-03-03 12:23:07 +01:00
b2cc3fe980 Merge pull request 'fixed failing tests' (#608) from po-607 into main
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline failed
ci/woodpecker/push/gateway_tron Pipeline failed
ci/woodpecker/push/ledger Pipeline is pending
ci/woodpecker/push/notification Pipeline is pending
ci/woodpecker/push/payments_methods Pipeline is pending
ci/woodpecker/push/payments_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #608
2026-03-03 10:53:28 +00:00
Stephan D
d28e8615a9 fixed failing tests 2026-03-03 11:53:04 +01:00
3d1157a5d3 Merge pull request 'improved logging in callbacks' (#606) from callbacks-604 into main
Some checks failed
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline failed
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #606
2026-03-03 00:08:01 +00:00
Stephan D
bae4cd6e35 improved logging in callbacks 2026-03-03 01:07:35 +01:00
bd79eb016a Merge pull request 'improved logging in callbacks' (#605) from callbacks-604 into main
Some checks failed
ci/woodpecker/push/callbacks Pipeline failed
Reviewed-on: #605
2026-03-03 00:07:03 +00:00
Stephan D
b10ec79fe0 improved logging in callbacks 2026-03-03 00:26:51 +01:00
4b57550c36 Merge pull request 'fixed front connection address' (#603) from front-600 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #603
2026-03-02 23:17:02 +00:00
Stephan D
0f0529c445 fixed front connection address 2026-03-03 00:16:37 +01:00
01c4108157 Merge pull request 'changed known policies enum' (#602) from front-600 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #602
2026-03-02 22:58:09 +00:00
Stephan D
3c6cffdf33 changed known policies enum 2026-03-02 23:57:27 +01:00
82bab11a8f Merge pull request 'changed known policies enum' (#601) from front-600 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #601
2026-03-02 22:50:51 +00:00
Stephan D
2f77d9d972 changed known policies enum 2026-03-02 23:48:59 +01:00
7559d4d09b Merge pull request 'changed color theme to be black and added the ability to enter the amount in the recipient’s currency' (#597) from SEND060 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #597
2026-03-02 17:47:56 +00:00
a1e739ba52 Merge pull request 'Callbacks service docs updated' (#598) from callbacks-596 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #598
2026-03-02 15:28:26 +00:00
Stephan D
2be76aa519 Callbacks service docs updated 2026-03-02 16:27:33 +01:00
Arseni
6bb3ab5063 changed color theme to be black and added the ability to enter the amount in the recipient’s currency 2026-03-02 17:41:41 +03:00
17e08ff26f Merge pull request 'added service reannounce' (#595) from discovery-593 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/callbacks Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #595
2026-03-01 13:47:10 +00:00
Stephan D
e5ba048c73 added service reannounce 2026-03-01 14:46:42 +01:00
1007 changed files with 46223 additions and 9404 deletions

View File

@@ -1,2 +1,18 @@
ci/dev/mongo.key* ci/dev/mongo.key*
# VCS / editor files
.git
.vscode
.DS_Store
**/.DS_Store
# Local caches and temporary artifacts
.cache
.gocache
**/.gocache
**/tmp/
**/tmp/**
# Frontend local build artifacts (rebuilt in Docker)
frontend/**/.dart_tool
frontend/**/build

4
.gitignore vendored
View File

@@ -10,9 +10,13 @@ generate_protos.sh
update_dep.sh update_dep.sh
test.sh test.sh
.vscode/ .vscode/
.gocache/ .gocache/
.golangci-cache/
.cache/ .cache/
.claude/ .claude/
.codex/
# Air hot reload build artifacts # Air hot reload build artifacts
**/tmp/ **/tmp/

View File

@@ -7,6 +7,9 @@ matrix:
BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault
BFF_ENV: prod BFF_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -47,6 +50,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh bff
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -75,8 +88,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/bff/build-image.sh - sh ci/scripts/bff/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
DOCUMENTS_MONGO_SECRET_PATH: sendico/db DOCUMENTS_MONGO_SECRET_PATH: sendico/db
DOCUMENTS_ENV: prod DOCUMENTS_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -42,6 +45,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh billing_documents
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -70,8 +83,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/billing_documents/build-image.sh - sh ci/scripts/billing_documents/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
FEES_MONGO_SECRET_PATH: sendico/db FEES_MONGO_SECRET_PATH: sendico/db
FEES_ENV: prod FEES_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -42,6 +45,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh billing_fees
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -70,8 +83,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/billing_fees/build-image.sh - sh ci/scripts/billing_fees/build-image.sh
- name: deploy - name: deploy

View File

@@ -6,6 +6,9 @@ matrix:
CALLBACKS_VAULT_SECRET_PATH: sendico/edge/callbacks/vault CALLBACKS_VAULT_SECRET_PATH: sendico/edge/callbacks/vault
CALLBACKS_ENV: prod CALLBACKS_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -43,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh callbacks
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -71,8 +84,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/callbacks/build-image.sh - sh ci/scripts/callbacks/build-image.sh
- name: deploy - name: deploy

View File

@@ -4,6 +4,9 @@ matrix:
DISCOVERY_DOCKERFILE: ci/prod/compose/discovery.dockerfile DISCOVERY_DOCKERFILE: ci/prod/compose/discovery.dockerfile
DISCOVERY_ENV: prod DISCOVERY_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -41,6 +44,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh discovery
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -69,8 +82,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/discovery/build-image.sh - sh ci/scripts/discovery/build-image.sh
- name: deploy - name: deploy

View File

@@ -4,6 +4,9 @@ matrix:
FRONTEND_DOCKERFILE: ci/prod/compose/frontend.dockerfile FRONTEND_DOCKERFILE: ci/prod/compose/frontend.dockerfile
FRONTEND_ENV: prod FRONTEND_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -22,7 +25,8 @@ steps:
- name: version - name: version
image: alpine:latest image: alpine:latest
commands: commands:
- set -euo pipefail 2>/dev/null || set -eu - set -eu
- if set -o | grep -q pipefail 2>/dev/null; then set -o pipefail; fi
- apk add --no-cache git - apk add --no-cache git
- GIT_REV="$(git rev-parse --short HEAD)" - GIT_REV="$(git rev-parse --short HEAD)"
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)" - BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
@@ -50,10 +54,21 @@ steps:
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER - ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD - ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
- name: frontend-tests
image: ghcr.io/cirruslabs/flutter:stable
depends_on: [ version ]
commands:
- set -eu
- if set -o | grep -q pipefail 2>/dev/null; then set -o pipefail; fi
- flutter --version
- (cd frontend/pshared && flutter pub get && dart run build_runner build --delete-conflicting-outputs && flutter test)
- (cd frontend/pweb && flutter pub get && dart run build_runner build --delete-conflicting-outputs && flutter test)
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ version, secrets ] depends_on: [ frontend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/frontend/build-image.sh - sh ci/scripts/frontend/build-image.sh
- name: deploy - name: deploy

View File

@@ -8,6 +8,9 @@ matrix:
FX_NEEDS_NATS: "true" FX_NEEDS_NATS: "true"
FX_ENV: prod FX_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -47,6 +50,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh fx_ingestor
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -75,8 +88,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/fx/build-image.sh - sh ci/scripts/fx/build-image.sh
- name: deploy - name: deploy

View File

@@ -8,6 +8,9 @@ matrix:
FX_NEEDS_NATS: "true" FX_NEEDS_NATS: "true"
FX_ENV: prod FX_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -48,6 +51,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh fx_oracle
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -76,8 +89,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/fx/build-image.sh - sh ci/scripts/fx/build-image.sh
- name: deploy - name: deploy

View File

@@ -8,6 +8,9 @@ matrix:
CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/chain/vault CHAIN_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/chain/vault
CHAIN_GATEWAY_ENV: prod CHAIN_GATEWAY_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -46,6 +49,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_chain
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -74,8 +87,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "chain gateway image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/chain_gateway/build-image.sh - sh ci/scripts/chain_gateway/build-image.sh
- name: deploy - name: deploy

View File

@@ -7,6 +7,9 @@ matrix:
MNTX_GATEWAY_NATS_SECRET_PATH: sendico/nats MNTX_GATEWAY_NATS_SECRET_PATH: sendico/nats
MNTX_GATEWAY_MONGO_SECRET_PATH: sendico/db MNTX_GATEWAY_MONGO_SECRET_PATH: sendico/db
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -45,6 +48,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_mntx
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -73,8 +86,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/mntx/build-image.sh - sh ci/scripts/mntx/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
TGSETTLE_GATEWAY_MONGO_SECRET_PATH: sendico/db TGSETTLE_GATEWAY_MONGO_SECRET_PATH: sendico/db
TGSETTLE_GATEWAY_ENV: prod TGSETTLE_GATEWAY_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -43,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_tgsettle
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -71,8 +84,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/tgsettle/build-image.sh - sh ci/scripts/tgsettle/build-image.sh
- name: deploy - name: deploy

View File

@@ -8,6 +8,9 @@ matrix:
TRON_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/tron/vault TRON_GATEWAY_VAULT_SECRET_PATH: sendico/gateway/tron/vault
TRON_GATEWAY_ENV: prod TRON_GATEWAY_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -46,6 +49,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh gateway_tron
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -74,8 +87,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/tron_gateway/build-image.sh - sh ci/scripts/tron_gateway/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
LEDGER_MONGO_SECRET_PATH: sendico/db LEDGER_MONGO_SECRET_PATH: sendico/db
LEDGER_ENV: prod LEDGER_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -42,6 +45,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh ledger
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -70,8 +83,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/ledger/build-image.sh - sh ci/scripts/ledger/build-image.sh
- name: deploy - name: deploy

View File

@@ -8,6 +8,9 @@ matrix:
NOTIFICATION_TELEGRAM_SECRET_PATH: sendico/notification/telegram NOTIFICATION_TELEGRAM_SECRET_PATH: sendico/notification/telegram
NOTIFICATION_ENV: prod NOTIFICATION_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -45,6 +48,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh notification
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -73,8 +86,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/notification/build-image.sh - sh ci/scripts/notification/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
PAYMENTS_METHODS_MONGO_SECRET_PATH: sendico/db PAYMENTS_METHODS_MONGO_SECRET_PATH: sendico/db
PAYMENTS_METHODS_ENV: prod PAYMENTS_METHODS_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -43,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh payments_methods
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -71,8 +84,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/payments_methods/build-image.sh - sh ci/scripts/payments_methods/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
PAYMENTS_MONGO_SECRET_PATH: sendico/db PAYMENTS_MONGO_SECRET_PATH: sendico/db
PAYMENTS_ENV: prod PAYMENTS_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -43,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh payments_orchestrator
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -71,8 +84,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/payments_orchestrator/build-image.sh - sh ci/scripts/payments_orchestrator/build-image.sh
- name: deploy - name: deploy

View File

@@ -5,6 +5,9 @@ matrix:
PAYMENTS_QUOTATION_MONGO_SECRET_PATH: sendico/db PAYMENTS_QUOTATION_MONGO_SECRET_PATH: sendico/db
PAYMENTS_QUOTATION_ENV: prod PAYMENTS_QUOTATION_ENV: prod
labels:
platform: linux/amd64
when: when:
- event: push - event: push
branch: main branch: main
@@ -43,6 +46,16 @@ steps:
- export PATH="$(go env GOPATH)/bin:$PATH" - export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh - bash ci/scripts/proto/generate.sh
- name: backend-lint
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- CGO_ENABLED=0 go install github.com/golangci/golangci-lint/v2/cmd/golangci-lint@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- sh ci/scripts/common/run_backend_lint.sh payments_quotation
- name: backend-tests - name: backend-tests
image: golang:alpine image: golang:alpine
depends_on: [ proto ] depends_on: [ proto ]
@@ -71,8 +84,9 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ] depends_on: [ backend-lint, backend-tests, secrets ]
commands: commands:
- '[ "$(uname -m)" = "x86_64" ] || { echo "image build requires an amd64 runner"; exit 1; }'
- sh ci/scripts/payments_quotation/build-image.sh - sh ci/scripts/payments_quotation/build-image.sh
- name: deploy - name: deploy

243
Makefile
View File

@@ -1,10 +1,83 @@
# Sendico Development Environment - Makefile # Sendico Development Environment - Makefile
# Docker Compose + Makefile build system # Docker Compose + Makefile build system
.PHONY: help init build up down restart logs rebuild clean vault-init proto generate generate-api generate-frontend update update-api update-frontend test test-api test-frontend .PHONY: \
help \
init \
build \
build-infra \
build-core \
build-fx \
build-payments \
build-gateways \
build-backend \
build-frontend \
up \
down \
restart \
infra-up \
services-up \
backend-up \
backend-down \
backend-rebuild \
status \
list-services \
logs \
rebuild \
clean \
health \
vault-init \
proto \
generate \
generate-backend \
generate-frontend \
update \
update-backend \
update-frontend \
test \
test-backend \
test-frontend \
lint-backend
COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev COMPOSE := docker compose -f docker-compose.dev.yml --env-file .env.dev
SERVICE ?= SERVICE ?=
BACKEND_GOCACHE ?= $(CURDIR)/.gocache
BACKEND_GOLANGCI_LINT_CACHE ?= $(CURDIR)/.golangci-cache
BACKEND_WAIT_TIMEOUT ?= 600
BACKEND_SERVICES := \
dev-discovery \
dev-fx-oracle \
dev-fx-ingestor \
dev-billing-fees \
dev-billing-documents \
dev-ledger \
dev-payments-orchestrator \
dev-payments-quotation \
dev-payments-methods \
dev-chain-gateway-vault-agent \
dev-chain-gateway \
dev-tron-gateway-vault-agent \
dev-tron-gateway \
dev-aurora-gateway \
dev-chsettle-gateway \
dev-notification \
dev-callbacks-vault-agent \
dev-callbacks \
dev-bff-vault-agent \
dev-bff
BACKEND_STAGE_DISCOVERY := dev-discovery
BACKEND_STAGE_FX := dev-fx-oracle dev-fx-ingestor
BACKEND_STAGE_BILLING := dev-billing-fees dev-billing-documents
BACKEND_STAGE_GATEWAY_SIDECARS := dev-chain-gateway-vault-agent dev-tron-gateway-vault-agent
BACKEND_STAGE_GATEWAYS_LEDGER := dev-chain-gateway dev-tron-gateway dev-aurora-gateway dev-chsettle-gateway dev-ledger
BACKEND_STAGE_QUOTATION := dev-payments-quotation
BACKEND_STAGE_PAYMENTS_CORE := dev-payments-methods dev-payments-orchestrator
BACKEND_STAGE_CALLBACKS_AGENT := dev-callbacks-vault-agent
BACKEND_STAGE_CALLBACKS := dev-callbacks
BACKEND_STAGE_EDGE_FOUNDATION := dev-notification dev-bff-vault-agent
BACKEND_STAGE_EDGE := dev-bff
BACKEND_STAGE_EDGE_BUILD := dev-notification dev-bff
FRONTEND_SERVICE := dev-frontend
# Colors # Colors
GREEN := \033[0;32m GREEN := \033[0;32m
@@ -24,34 +97,38 @@ help:
@echo " make down Stop all services" @echo " make down Stop all services"
@echo " make restart Restart all services" @echo " make restart Restart all services"
@echo " make status Show service status" @echo " make status Show service status"
@echo " make logs [SERVICE=x] View logs (all or specific service)" @echo " make logs [SERVICE=dev-ledger] View logs (all or specific service)"
@echo " make rebuild SERVICE=x Rebuild specific service" @echo " make rebuild SERVICE=dev-ledger Rebuild specific service"
@echo " make clean Remove all containers and volumes" @echo " make clean Remove all containers and volumes"
@echo "" @echo ""
@echo "$(YELLOW)Selective Operations:$(NC)" @echo "$(YELLOW)Selective Operations:$(NC)"
@echo " make infra-up Start infrastructure only (mongo, nats, vault)" @echo " make infra-up Start infrastructure only (mongo, nats, vault)"
@echo " make services-up Start application services only" @echo " make services-up Start application services only (ordered backend stages)"
@echo " make backend-up Start backend services only in ordered stages (no infrastructure/frontend)"
@echo " make backend-down Stop backend services only"
@echo " make backend-rebuild Rebuild and restart backend services in ordered stages"
@echo " make list-services List all available services" @echo " make list-services List all available services"
@echo "" @echo ""
@echo "$(YELLOW)Build Groups:$(NC)" @echo "$(YELLOW)Build Groups:$(NC)"
@echo " make build-core Build core services (discovery, ledger, fees, documents)" @echo " make build-core Build core services (discovery, ledger, fees, documents)"
@echo " make build-fx Build FX services (oracle, ingestor)" @echo " make build-fx Build FX services (oracle, ingestor)"
@echo " make build-payments Build payment orchestrator" @echo " make build-payments Build payment services (orchestrator, quotation, methods)"
@echo " make build-gateways Build gateway services (chain, tron, mntx, tgsettle)" @echo " make build-gateways Build gateway services (chain, tron, aurora, chsettle)"
@echo " make build-api Build API services (notification, callbacks, bff)" @echo " make build-backend Build backend edge services (notification, callbacks, bff)"
@echo " make build-frontend Build Flutter web frontend" @echo " make build-frontend Build Flutter web frontend"
@echo "" @echo ""
@echo "$(YELLOW)Development:$(NC)" @echo "$(YELLOW)Development:$(NC)"
@echo " make proto Generate protobuf code" @echo " make proto Generate protobuf code"
@echo " make generate Generate all code (protobuf + Flutter)" @echo " make generate Generate all code (protobuf + Flutter)"
@echo " make generate-api Generate protobuf code only" @echo " make generate-backend Generate protobuf code only"
@echo " make generate-frontend Generate Flutter code only" @echo " make generate-frontend Generate Flutter code only"
@echo " make update Update all dependencies (Go + Flutter)" @echo " make update Update all dependencies (Go + Flutter)"
@echo " make update-api Update Go dependencies only" @echo " make update-backend Update Go dependencies only"
@echo " make update-frontend Update Flutter dependencies only" @echo " make update-frontend Update Flutter dependencies only"
@echo " make test Run all tests (API + frontend)" @echo " make test Run all tests (backend + frontend)"
@echo " make test-api Run Go API tests only" @echo " make test-backend Run Go backend tests only"
@echo " make test-frontend Run Flutter tests only" @echo " make test-frontend Run Flutter tests only"
@echo " make lint-backend Run golangci-lint across all backend Go modules"
@echo " make health Check service health" @echo " make health Check service health"
@echo "" @echo ""
@echo "Examples:" @echo "Examples:"
@@ -103,7 +180,8 @@ build:
# Start all services # Start all services
up: up:
@echo "$(GREEN)Starting development environment...$(NC)" @echo "$(GREEN)Starting development environment...$(NC)"
@$(COMPOSE) up -d @$(MAKE) --no-print-directory infra-up
@$(MAKE) --no-print-directory services-up
@echo "" @echo ""
@echo "$(GREEN)✅ Development environment started!$(NC)" @echo "$(GREEN)✅ Development environment started!$(NC)"
@echo "" @echo ""
@@ -113,7 +191,7 @@ up:
@echo " NATS UI: http://localhost:8222" @echo " NATS UI: http://localhost:8222"
@echo " Vault: http://localhost:8200 (run 'make vault-init' first)" @echo " Vault: http://localhost:8200 (run 'make vault-init' first)"
@echo "" @echo ""
@echo "View logs: make logs [SERVICE=name]" @echo "View logs: make logs [SERVICE=dev-ledger]"
@echo "Stop: make down" @echo "Stop: make down"
# Stop all services # Stop all services
@@ -137,7 +215,7 @@ endif
# Rebuild specific service # Rebuild specific service
rebuild: rebuild:
ifndef SERVICE ifndef SERVICE
$(error SERVICE is required: make rebuild SERVICE=ledger) $(error SERVICE is required: make rebuild SERVICE=dev-ledger)
endif endif
@echo "$(GREEN)Rebuilding $(SERVICE)...$(NC)" @echo "$(GREEN)Rebuilding $(SERVICE)...$(NC)"
@$(COMPOSE) build $(SERVICE) @$(COMPOSE) build $(SERVICE)
@@ -146,13 +224,13 @@ endif
@echo "View logs: make logs SERVICE=$(SERVICE)" @echo "View logs: make logs SERVICE=$(SERVICE)"
# Generate protobuf code (alias) # Generate protobuf code (alias)
proto: generate-api proto: generate-backend
# Generate all code # Generate all code
generate: generate-api generate-frontend generate: generate-backend generate-frontend
# Generate protobuf code # Generate backend protobuf code
generate-api: generate-backend:
@echo "$(GREEN)Generating protobuf code...$(NC)" @echo "$(GREEN)Generating protobuf code...$(NC)"
@./ci/scripts/proto/generate.sh @./ci/scripts/proto/generate.sh
@echo "$(GREEN)✅ Protobuf generation complete$(NC)" @echo "$(GREEN)✅ Protobuf generation complete$(NC)"
@@ -209,25 +287,75 @@ infra-up:
# Services only (assumes infra is running) # Services only (assumes infra is running)
services-up: services-up:
@echo "$(GREEN)Starting application services...$(NC)" @echo "$(GREEN)Starting application services with ordered backend stages...$(NC)"
@$(COMPOSE) up -d \ @$(MAKE) --no-print-directory backend-up
dev-discovery \ @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(FRONTEND_SERVICE)
dev-fx-oracle \
dev-fx-ingestor \ # Backend services only (no infrastructure, no frontend)
dev-billing-fees \ backend-up:
dev-billing-documents \ @echo "$(GREEN)Starting backend services only (ordered build+start stages, no infra changes)...$(NC)"
dev-ledger \ @echo "$(YELLOW)[1/8] discovery$(NC)"
dev-payments-orchestrator \ @$(COMPOSE) build $(BACKEND_STAGE_DISCOVERY)
dev-payments-quotation \ @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_DISCOVERY)
dev-payments-methods \ @echo "$(YELLOW)[2/8] fx (oracle + ingestor)$(NC)"
dev-chain-gateway \ @$(COMPOSE) build $(BACKEND_STAGE_FX)
dev-tron-gateway \ @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_FX)
dev-mntx-gateway \ @echo "$(YELLOW)[3/8] billing (fees + documents)$(NC)"
dev-tgsettle-gateway \ @$(COMPOSE) build $(BACKEND_STAGE_BILLING)
dev-notification \ @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_BILLING)
dev-callbacks \ @echo "$(YELLOW)[4/8] gateways + ledger$(NC)"
dev-bff \ @$(COMPOSE) build $(BACKEND_STAGE_GATEWAYS_LEDGER)
dev-frontend @$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAY_SIDECARS)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAYS_LEDGER)
@echo "$(YELLOW)[5/8] quotation$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_QUOTATION)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_QUOTATION)
@echo "$(YELLOW)[6/8] orchestrator + methods$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_PAYMENTS_CORE)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_PAYMENTS_CORE)
@echo "$(YELLOW)[7/8] callbacks$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_CALLBACKS)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS_AGENT)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS)
@echo "$(YELLOW)[8/8] edge (notification + bff)$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_EDGE_BUILD)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE_FOUNDATION)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE)
backend-down:
@echo "$(YELLOW)Stopping backend services only...$(NC)"
@$(COMPOSE) stop $(BACKEND_SERVICES)
backend-rebuild:
@echo "$(GREEN)Rebuilding backend services only (ordered stages, no infra changes)...$(NC)"
@echo "$(YELLOW)[1/8] discovery$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_DISCOVERY)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_DISCOVERY)
@echo "$(YELLOW)[2/8] fx (oracle + ingestor)$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_FX)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_FX)
@echo "$(YELLOW)[3/8] billing (fees + documents)$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_BILLING)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_BILLING)
@echo "$(YELLOW)[4/8] gateways + ledger$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_GATEWAYS_LEDGER)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAY_SIDECARS)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_GATEWAYS_LEDGER)
@echo "$(YELLOW)[5/8] quotation$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_QUOTATION)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_QUOTATION)
@echo "$(YELLOW)[6/8] orchestrator + methods$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_PAYMENTS_CORE)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_PAYMENTS_CORE)
@echo "$(YELLOW)[7/8] callbacks$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_CALLBACKS)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS_AGENT)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_CALLBACKS)
@echo "$(YELLOW)[8/8] edge (notification + bff)$(NC)"
@$(COMPOSE) build $(BACKEND_STAGE_EDGE_BUILD)
@$(COMPOSE) up -d --no-deps --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE_FOUNDATION)
@$(COMPOSE) up -d --no-deps --force-recreate --wait --wait-timeout $(BACKEND_WAIT_TIMEOUT) $(BACKEND_STAGE_EDGE)
@echo "$(GREEN)✅ Backend services rebuilt$(NC)"
# Status check # Status check
status: status:
@@ -252,8 +380,8 @@ list-services:
@echo " - dev-payments-methods :50066, :9416 (Payment Methods)" @echo " - dev-payments-methods :50066, :9416 (Payment Methods)"
@echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)" @echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)"
@echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)" @echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)"
@echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)" @echo " - dev-aurora-gateway :50075, :9405, :8084 (Card Payouts Simulator)"
@echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)" @echo " - dev-chsettle-gateway :50080, :9406 (Chimera Settlements Simulator)"
@echo " - dev-notification :8081 (Notifications)" @echo " - dev-notification :8081 (Notifications)"
@echo " - dev-callbacks :9420 (Webhook Callbacks)" @echo " - dev-callbacks :9420 (Webhook Callbacks)"
@echo " - dev-bff :8080 (Backend for Frontend)" @echo " - dev-bff :8080 (Backend for Frontend)"
@@ -283,10 +411,10 @@ build-payments:
build-gateways: build-gateways:
@echo "$(GREEN)Building gateway services...$(NC)" @echo "$(GREEN)Building gateway services...$(NC)"
@$(COMPOSE) build dev-chain-gateway dev-tron-gateway dev-mntx-gateway dev-tgsettle-gateway @$(COMPOSE) build dev-chain-gateway dev-tron-gateway dev-aurora-gateway dev-chsettle-gateway
build-api: build-backend:
@echo "$(GREEN)Building API services...$(NC)" @echo "$(GREEN)Building backend edge services...$(NC)"
@$(COMPOSE) build dev-notification dev-callbacks dev-bff @$(COMPOSE) build dev-notification dev-callbacks dev-bff
build-frontend: build-frontend:
@@ -294,10 +422,10 @@ build-frontend:
@$(COMPOSE) build dev-frontend @$(COMPOSE) build dev-frontend
# Update all dependencies # Update all dependencies
update: update-api update-frontend update: update-backend update-frontend
# Update Go API dependencies # Update Go backend dependencies
update-api: update-backend:
@echo "$(GREEN)Updating Go dependencies...$(NC)" @echo "$(GREEN)Updating Go dependencies...$(NC)"
@for dir in $$(find api -name go.mod -exec dirname {} \;); do \ @for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Updating $$dir..."; \ echo "Updating $$dir..."; \
@@ -313,11 +441,11 @@ update-frontend:
@echo "$(GREEN)✅ Flutter dependencies updated$(NC)" @echo "$(GREEN)✅ Flutter dependencies updated$(NC)"
# Run all tests # Run all tests
test: test-api test-frontend test: test-backend test-frontend
# Run Go API tests # Run Go backend tests
test-api: test-backend:
@echo "$(GREEN)Running API tests...$(NC)" @echo "$(GREEN)Running backend tests...$(NC)"
@failed=""; \ @failed=""; \
for dir in $$(find api -name go.mod -exec dirname {} \;); do \ for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Testing $$dir..."; \ echo "Testing $$dir..."; \
@@ -327,7 +455,7 @@ test-api:
echo "$(YELLOW)Failed:$$failed$(NC)"; \ echo "$(YELLOW)Failed:$$failed$(NC)"; \
exit 1; \ exit 1; \
fi fi
@echo "$(GREEN)✅ All API tests passed$(NC)" @echo "$(GREEN)✅ All backend tests passed$(NC)"
# Run Flutter tests # Run Flutter tests
test-frontend: test-frontend:
@@ -335,3 +463,18 @@ test-frontend:
@cd frontend/pshared && flutter test @cd frontend/pshared && flutter test
@cd frontend/pweb && flutter test @cd frontend/pweb && flutter test
@echo "$(GREEN)✅ All frontend tests passed$(NC)" @echo "$(GREEN)✅ All frontend tests passed$(NC)"
# Run Go backend linting
lint-backend:
@echo "$(GREEN)Running backend linting...$(NC)"
@mkdir -p "$(BACKEND_GOCACHE)" "$(BACKEND_GOLANGCI_LINT_CACHE)"
@failed=""; \
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Linting $$dir..."; \
(cd "$$dir" && GOCACHE="$(BACKEND_GOCACHE)" GOLANGCI_LINT_CACHE="$(BACKEND_GOLANGCI_LINT_CACHE)" golangci-lint run --allow-serial-runners --allow-parallel-runners ./...) || failed="$$failed $$dir"; \
done; \
if [ -n "$$failed" ]; then \
echo "$(YELLOW)Lint failed:$$failed$(NC)"; \
exit 1; \
fi
@echo "$(GREEN)✅ All backend lint checks passed$(NC)"

View File

@@ -24,13 +24,25 @@ Financial services platform providing payment orchestration, ledger accounting,
| FX Ingestor | `api/fx/ingestor/` | FX rate ingestion | | FX Ingestor | `api/fx/ingestor/` | FX rate ingestion |
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway | | Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway | | Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
| Gateway Aurora | `api/gateway/aurora/` | Card payouts simulator |
| Gateway ChimeraSettle | `api/gateway/chsettle/` | Dummy settlement simulator (fast/slow/success/fail/stuck) |
| Gateway MNTX | `api/gateway/mntx/` | Card payouts | | Gateway MNTX | `api/gateway/mntx/` | Card payouts |
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX | | Gateway TGSettle (legacy) | `api/gateway/tgsettle/` | Legacy Telegram settlement gateway (not used in dev compose) |
| Notification | `api/notification/` | Notifications | | Notification | `api/notification/` | Notifications |
| BFF | `api/edge/bff/` | Backend for frontend | | BFF | `api/edge/bff/` | Backend for frontend |
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery | | Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
| Frontend | `frontend/pweb/` | Flutter web UI | | Frontend | `frontend/pweb/` | Flutter web UI |
Gateway note: current dev compose workflows (`make services-up`, `make build-gateways`) use (`chain`, `tron`, `aurora`, `chsettle`). ChimeraSettle is the settlement simulator for test flows; it supports deterministic behavior routing via explicit scenario override and amount buckets. TGSettle remains in-repo as legacy code and is not started by default in dev compose.
## Prerequisites
- Docker with Docker Compose plugin
- GNU Make
- Go toolchain
- Dart SDK
- Flutter SDK
## Development ## Development
Development uses Docker Compose via the Makefile. Run `make help` for all available commands. Development uses Docker Compose via the Makefile. Run `make help` for all available commands.
@@ -54,6 +66,8 @@ make status # Show service status
make logs # View all logs make logs # View all logs
make logs SERVICE=dev-ledger # View logs for a specific service make logs SERVICE=dev-ledger # View logs for a specific service
make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service
make list-services # List all services and ports
make health # Check service health
make clean # Remove all containers and volumes make clean # Remove all containers and volumes
``` ```
@@ -62,6 +76,10 @@ make clean # Remove all containers and volumes
```bash ```bash
make infra-up # Start infrastructure only (MongoDB, NATS, Vault) make infra-up # Start infrastructure only (MongoDB, NATS, Vault)
make services-up # Start application services only (assumes infra is running) make services-up # Start application services only (assumes infra is running)
make backend-up # Start backend services only (no infrastructure/frontend changes)
make backend-down # Stop backend services only
make backend-rebuild # Rebuild and restart backend services only
make list-services # Show service names, ports, and descriptions
``` ```
### Build Groups ### Build Groups
@@ -69,9 +87,9 @@ make services-up # Start application services only (assumes infra is running)
```bash ```bash
make build-core # discovery, ledger, fees, documents make build-core # discovery, ledger, fees, documents
make build-fx # oracle, ingestor make build-fx # oracle, ingestor
make build-payments # orchestrator make build-payments # orchestrator, quotation, methods
make build-gateways # chain, tron, mntx, tgsettle make build-gateways # chain, tron, aurora, chsettle
make build-api # notification, callbacks, bff make build-backend # notification, callbacks, bff
make build-frontend # Flutter web UI make build-frontend # Flutter web UI
``` ```
@@ -79,24 +97,43 @@ make build-frontend # Flutter web UI
```bash ```bash
make generate # Generate all code (protobuf + Flutter) make generate # Generate all code (protobuf + Flutter)
make generate-api # Generate protobuf code only make generate-backend # Generate protobuf code only
make generate-frontend # Generate Flutter code only (build_runner) make generate-frontend # Generate Flutter code only (build_runner)
make proto # Alias for generate-api make proto # Alias for generate-backend
``` ```
### Testing ### Testing
```bash ```bash
make test # Run all tests (API + frontend) make test # Run all tests (backend + frontend)
make test-api # Run Go API tests only make test-backend # Run Go backend tests only
make test-frontend # Run Flutter tests only make test-frontend # Run Flutter tests only
``` ```
### Backend CI Bypass Tags
Backend Woodpecker module pipelines now run both lint and tests before image build/deploy.
If you intentionally need to bypass checks for a specific commit, include one of these tags in the commit message:
```text
[skip-lint:<service>] # Skip lint for one backend service pipeline
[skip-lint] # Skip lint for all backend service pipelines
[skip-tests:<service>] # Skip tests for one backend service pipeline
[skip-tests] # Skip tests for all backend service pipelines
[skip-autotests:<service>] # Alias for skip-tests:<service>
[skip-autotests] # Alias for skip-tests
[skip-checks:<service>] # Skip both lint and tests for one backend service pipeline
[skip-checks] # Skip both lint and tests for all backend service pipelines
```
`<service>` must match the backend pipeline key used by CI:
`bff`, `callbacks`, `billing_documents`, `billing_fees`, `discovery`, `fx_ingestor`, `fx_oracle`, `gateway_chain`, `gateway_mntx`, `gateway_tgsettle`, `gateway_tron`, `ledger`, `notification`, `payments_methods`, `payments_orchestrator`, `payments_quotation`.
### Update Dependencies ### Update Dependencies
```bash ```bash
make update # Update all Go and Flutter dependencies make update # Update all Go and Flutter dependencies
make update-api # Update Go dependencies only make update-backend # Update Go dependencies only
make update-frontend # Update Flutter dependencies only make update-frontend # Update Flutter dependencies only
``` ```

View File

@@ -137,10 +137,11 @@ make infra-up
make services-up make services-up
# Or start specific service groups # Or start specific service groups
make build-core # discovery, ledger, billing-fees make build-core # discovery, ledger, billing-fees, billing-documents
make build-fx # fx-oracle, fx-ingestor make build-fx # fx-oracle, fx-ingestor
make build-payments # payments-orchestrator make build-payments # payments-orchestrator, payments-quotation, payments-methods
make build-gateways # chain, mntx, tgsettle make build-gateways # chain, tron, aurora, chsettle
make build-backend # notification, callbacks, bff
``` ```
--- ---

View File

@@ -1,196 +1,47 @@
# See the dedicated "version" documentation section.
version: "2" version: "2"
linters: linters:
# Default set of linters. default: none
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable: enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose - bodyclose
- canonicalheader - canonicalheader
- containedctx
- contextcheck
- copyloopvar - copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck - durationcheck
- embeddedstructfieldcheck
- err113
- errcheck - errcheck
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec - gosec
- gosmopolitan
- govet - govet
- grouper
- iface
- importas
- inamedparam
- ineffassign - ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr - nilerr
- nilnesserr - nilnesserr
- nilnil - nilnil
- nlreturn
- noctx - noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck - rowserrcheck
- sloglint
- spancheck
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert - unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign - wastedassign
- whitespace disable:
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard - depguard
- exhaustruct - exhaustruct
- gochecknoglobals - gochecknoglobals
- gochecknoinits
- gomoddirectives - gomoddirectives
- wsl
- wrapcheck - wrapcheck
# All available settings of specific linters. - cyclop
# See the dedicated "linters.settings" documentation section. - dupl
settings: - funlen
wsl_v5: - gocognit
allow-first-in-block: true - gocyclo
allow-whole-block: false - ireturn
branch-max-lines: 2 - lll
- mnd
# Defines a set of rules to ignore issues. - nestif
# It does not skip the analysis, and so does not ignore "typecheck" errors. - nlreturn
exclusions: - noinlineerr
# Mode of the generated files analysis. - paralleltest
# - tagliatelle
# - `strict`: sources are excluded by strictly following the Go generated file convention. - testpackage
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - varnamelen
# This line must appear before the first non-comment, non-blank text in the file. - wsl_v5
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -24,9 +24,7 @@ database:
documents: documents:
issuer: issuer:
legal_name: "Sendico Ltd" logo_path: "assets/logo.png"
legal_address: "12 Market Street, London, UK"
logo_path: "/assets/logo.png"
templates: templates:
acceptance_path: "templates/acceptance.tpl" acceptance_path: "templates/acceptance.tpl"
protection: protection:

View File

@@ -24,11 +24,9 @@ database:
documents: documents:
issuer: issuer:
legal_name: "Sendico Ltd" logo_path: "/app/assets/logo.png"
legal_address: "12 Market Street, London, UK"
logo_path: "/assets/logo.png"
templates: templates:
acceptance_path: "templates/acceptance.tpl" acceptance_path: "/app/templates/acceptance.tpl"
protection: protection:
owner_password: "sendico-documents" owner_password: "sendico-documents"
storage: storage:

View File

@@ -5,35 +5,35 @@ go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
require ( require (
github.com/aws/aws-sdk-go-v2 v1.41.2 github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0
github.com/jung-kurt/gofpdf v1.16.2 github.com/jung-kurt/gofpdf v1.16.2
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.79.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
require ( require (
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.2 // indirect github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect
@@ -59,12 +59,12 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -4,42 +4,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0 h1:zyKY4OxzUImu+DigelJI9o49QQv8CjREs5E1CywjtIA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
@@ -217,40 +217,40 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0= golang.org/x/image v0.0.0-20190910094157-69e4b8554b2a/go.mod h1:FeLwcggjj3mMvU+oOTbSwawSJRM1uh48EjtB4UJZlP0=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -258,10 +258,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -0,0 +1,151 @@
package content
// Issuer details are intentionally centralized to avoid document text drift.
const (
IssuerLegalName = "SMX Operations Limited"
IssuerLegalAddress = "Room 607, 12/F., Block C, Hong Kong Industrial Centre, 489-491 Castle Peak Road, Lai Chi Kok, HongKong"
IssuerEmail = "contact@sendico.io"
)
const (
PDFTitleActOfAcceptance = "Act of Acceptance"
DocumentIntegrityHashPrefix = "Document integrity hash: "
)
// AcceptanceTemplateContent contains all static copy used by the acceptance act template.
type AcceptanceTemplateContent struct {
Title string
Subtitle string
MetaDateLabel string
MetaActNumberLabel string
SectionParties string
PartiesIntro string
PartyExecutorLabel string
PartyStatusLabel string
PartyStatusValue string
SectionBasis string
BasisLine1 string
BasisLine2 string
SectionServicesRendered string
ServicesRenderedLine1 string
ServicesRenderedLine2 string
SectionRemuneration string
RemunerationHeaderDesc string
RemunerationHeaderAmount string
RemunerationServicesDesc string
SectionConfirmation string
ConfirmationLine1 string
ConfirmationLine2 string
ConfirmationPaymentLine1 string
ConfirmationPaymentLine2 string
SectionSignatures string
SignatureCustomerLine string
SignatureExecutorLine string
}
var AcceptanceTemplate = AcceptanceTemplateContent{
Title: "ACT OF ACCEPTANCE OF SERVICES",
Subtitle: "under the Public Offer Agreement",
MetaDateLabel: "Date",
MetaActNumberLabel: "Act No",
SectionParties: "PARTIES",
PartiesIntro: "This Act is made between the following Parties.",
PartyExecutorLabel: "Executor",
PartyStatusLabel: "Status",
PartyStatusValue: "Individual",
SectionBasis: "BASIS",
BasisLine1: "This Act is issued pursuant to the Public Offer Agreement",
BasisLine2: "accepted by the Executor by joining the offer.",
SectionServicesRendered: "SERVICES RENDERED",
ServicesRenderedLine1: "The Executor has rendered services to the Customer",
ServicesRenderedLine2: "in accordance with the terms of the Public Offer Agreement.",
SectionRemuneration: "REMUNERATION",
RemunerationHeaderDesc: "Description",
RemunerationHeaderAmount: "Amount",
RemunerationServicesDesc: "Services rendered under the Public Offer Agreement",
SectionConfirmation: "CONFIRMATION",
ConfirmationLine1: "The Customer confirms that the services were rendered properly",
ConfirmationLine2: "and accepted without any claims.",
ConfirmationPaymentLine1: "The remuneration for the services was paid to the Executor",
ConfirmationPaymentLine2: "using the bank card details provided by the Executor.",
SectionSignatures: "SIGNATURES",
SignatureCustomerLine: "Customer ___________________________",
SignatureExecutorLine: "Executor ___________________________",
}
// OperationDocumentContent contains all static copy for operation documents.
type OperationDocumentContent struct {
Title string
Subtitle string
MetaCertificateNumberLabel string
MetaDateLabel string
SectionParties string
PartiesIntro string
RowServiceProvider string
RowServiceProviderAddress string
RowServiceProviderEmail string
RowClient string
RowClientAddress string
RowClientReference string
SectionSubject string
SubjectIntro string
SectionServicePeriod string
RowPeriodFrom string
RowPeriodTo string
SectionTotalAmount string
RowTotalAmount string
SectionClientConfirmation string
ConfirmationLine1 string
ConfirmationLine2 string
ConfirmationLine3 string
SectionSignatures string
SignatureServiceProviderLine string
SignatureClientNamePrefix string
SignatureClientTitleLine string
SignatureClientLine string
SectionOperationStatus string
RowOperationStatus string
RowOperationCode string
RowOperationLabel string
RowFailureCode string
RowFailureReason string
MissingValuePlaceholder string
}
var OperationDocument = OperationDocumentContent{
Title: "CERTIFICATE OF SERVICES RENDERED",
Subtitle: "Payment operation completion and acceptance statement",
MetaCertificateNumberLabel: "Certificate No.",
MetaDateLabel: "Date",
SectionParties: "PARTIES",
PartiesIntro: "This Certificate is made between:",
RowServiceProvider: "Service Provider",
RowServiceProviderAddress: "Service Provider Address",
RowServiceProviderEmail: "Service Provider Email",
RowClient: "Client",
RowClientAddress: "Client Address",
RowClientReference: "Client Reference",
SectionSubject: "SUBJECT OF THE CERTIFICATE",
SubjectIntro: "The Service Provider confirms that the following services have been fully rendered to the Client:",
SectionServicePeriod: "SERVICE PERIOD",
RowPeriodFrom: "From",
RowPeriodTo: "To",
SectionTotalAmount: "TOTAL AMOUNT",
RowTotalAmount: "Amount",
SectionClientConfirmation: "CLIENT CONFIRMATION",
ConfirmationLine1: "- the services were rendered in full;",
ConfirmationLine2: "- the services were rendered properly and within the agreed scope;",
ConfirmationLine3: "- the Client has no claims regarding the quality, quantity, or timing of the services rendered.",
SectionSignatures: "SIGNATURES",
SignatureServiceProviderLine: "Service Provider: Name: SMX Operations Limited | Signature: __________________",
SignatureClientNamePrefix: "Client: Name:",
SignatureClientTitleLine: "Title: Authorized Representative",
SignatureClientLine: "Signature: __________________",
SectionOperationStatus: "OPERATION STATUS",
RowOperationStatus: "Operation Status",
RowOperationCode: "Operation Code",
RowOperationLabel: "Operation Label",
RowFailureCode: "Failure Code",
RowFailureReason: "Failure Reason",
MissingValuePlaceholder: "n/a",
}

View File

@@ -2,6 +2,7 @@ package docstore
import ( import (
"context" "context"
"fmt"
"os" "os"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -36,8 +37,12 @@ func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error {
return err return err
} }
path := filepath.Join(s.rootPath, filepath.Clean(key)) path, err := resolveStoragePath(s.rootPath, key)
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { if err != nil {
return err
}
if err := os.MkdirAll(filepath.Dir(path), 0o750); err != nil {
s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path)) s.logger.Warn("Failed to create document directory", zap.Error(err), zap.String("path", path))
return err return err
@@ -56,8 +61,12 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
return nil, err return nil, err
} }
path := filepath.Join(s.rootPath, filepath.Clean(key)) path, err := resolveStoragePath(s.rootPath, key)
if err != nil {
return nil, err
}
//nolint:gosec // path is constrained by resolveStoragePath to stay within configured root.
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path)) s.logger.Warn("Failed to read document file", zap.Error(err), zap.String("path", path))
@@ -69,3 +78,32 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
} }
var _ Store = (*LocalStore)(nil) var _ Store = (*LocalStore)(nil)
func resolveStoragePath(rootPath string, key string) (string, error) {
cleanKey := filepath.Clean(strings.TrimSpace(key))
if cleanKey == "" || cleanKey == "." {
return "", merrors.InvalidArgument("docstore: key is required")
}
if filepath.IsAbs(cleanKey) {
return "", merrors.InvalidArgument("docstore: absolute keys are not allowed")
}
rootAbs, err := filepath.Abs(rootPath)
if err != nil {
return "", fmt.Errorf("resolve local store root: %w", err)
}
path := filepath.Join(rootAbs, cleanKey)
pathAbs, err := filepath.Abs(path)
if err != nil {
return "", fmt.Errorf("resolve local store path: %w", err)
}
prefix := rootAbs + string(filepath.Separator)
if pathAbs != rootAbs && !strings.HasPrefix(pathAbs, prefix) {
return "", merrors.InvalidArgument("docstore: key escapes root path")
}
return pathAbs, nil
}

View File

@@ -124,7 +124,11 @@ func (s *S3Store) Load(ctx context.Context, key string) ([]byte, error) {
return nil, err return nil, err
} }
defer obj.Body.Close() defer func() {
if closeErr := obj.Body.Close(); closeErr != nil {
s.logger.Warn("Failed to close document body", zap.Error(closeErr), zap.String("key", key))
}
}()
return io.ReadAll(obj.Body) return io.ReadAll(obj.Body)
} }

View File

@@ -3,18 +3,24 @@ package documents
import ( import (
"strings" "strings"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/internal/docstore" "github.com/tech/sendico/billing/documents/internal/docstore"
"github.com/tech/sendico/billing/documents/renderer" "github.com/tech/sendico/billing/documents/renderer"
) )
// Config holds document service settings loaded from YAML. // Config holds document service settings loaded from YAML.
type Config struct { type Config struct {
Issuer renderer.Issuer `yaml:"issuer"` Issuer IssuerConfig `yaml:"issuer"`
Templates TemplateConfig `yaml:"templates"` Templates TemplateConfig `yaml:"templates"`
Protection ProtectionConfig `yaml:"protection"` Protection ProtectionConfig `yaml:"protection"`
Storage docstore.Config `yaml:"storage"` Storage docstore.Config `yaml:"storage"`
} }
// IssuerConfig defines issuer settings that are environment-specific.
type IssuerConfig struct {
LogoPath string `yaml:"logo_path"`
}
// TemplateConfig defines document template locations. // TemplateConfig defines document template locations.
type TemplateConfig struct { type TemplateConfig struct {
AcceptancePath string `yaml:"acceptance_path"` AcceptancePath string `yaml:"acceptance_path"`
@@ -25,6 +31,14 @@ type ProtectionConfig struct {
OwnerPassword string `yaml:"owner_password"` OwnerPassword string `yaml:"owner_password"`
} }
func (c Config) IssuerDetails() renderer.Issuer {
return renderer.Issuer{
LegalName: content.IssuerLegalName,
LegalAddress: content.IssuerLegalAddress,
LogoPath: c.Issuer.LogoPath,
}
}
func (c Config) AcceptanceTemplatePath() string { func (c Config) AcceptanceTemplatePath() string {
if strings.TrimSpace(c.Templates.AcceptancePath) == "" { if strings.TrimSpace(c.Templates.AcceptancePath) == "" {
return "templates/acceptance.tpl" return "templates/acceptance.tpl"

View File

@@ -7,7 +7,6 @@ import (
"github.com/prometheus/client_golang/prometheus" "github.com/prometheus/client_golang/prometheus"
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
"google.golang.org/grpc/codes" "google.golang.org/grpc/codes"
"google.golang.org/grpc/status" "google.golang.org/grpc/status"
) )
@@ -17,7 +16,6 @@ var (
requestsTotal *prometheus.CounterVec requestsTotal *prometheus.CounterVec
requestLatency *prometheus.HistogramVec requestLatency *prometheus.HistogramVec
batchSize prometheus.Histogram
documentBytes *prometheus.HistogramVec documentBytes *prometheus.HistogramVec
) )
@@ -44,16 +42,6 @@ func initMetrics() {
[]string{"call", "status", "doc_type"}, []string{"call", "status", "doc_type"},
) )
batchSize = promauto.NewHistogram(
prometheus.HistogramOpts{
Namespace: "billing",
Subsystem: "documents",
Name: "batch_size",
Help: "Number of payment references in batch resolution requests.",
Buckets: []float64{0, 1, 2, 5, 10, 20, 50, 100, 250, 500},
},
)
documentBytes = promauto.NewHistogramVec( documentBytes = promauto.NewHistogramVec(
prometheus.HistogramOpts{ prometheus.HistogramOpts{
Namespace: "billing", Namespace: "billing",
@@ -67,18 +55,14 @@ func initMetrics() {
}) })
} }
func observeRequest(call string, docType documentsv1.DocumentType, statusLabel string, took time.Duration) { func observeRequest(call string, documentKind, statusLabel string, took time.Duration) {
typeLabel := docTypeLabel(docType) kind := docKindLabel(documentKind)
requestsTotal.WithLabelValues(call, statusLabel, typeLabel).Inc() requestsTotal.WithLabelValues(call, statusLabel, kind).Inc()
requestLatency.WithLabelValues(call, statusLabel, typeLabel).Observe(took.Seconds()) requestLatency.WithLabelValues(call, statusLabel, kind).Observe(took.Seconds())
} }
func observeBatchSize(size int) { func observeDocumentBytes(documentKind string, size int) {
batchSize.Observe(float64(size)) documentBytes.WithLabelValues(docKindLabel(documentKind)).Observe(float64(size))
}
func observeDocumentBytes(docType documentsv1.DocumentType, size int) {
documentBytes.WithLabelValues(docTypeLabel(docType)).Observe(float64(size))
} }
func statusFromError(err error) string { func statusFromError(err error) string {
@@ -100,10 +84,10 @@ func statusFromError(err error) string {
return strings.ToLower(code.String()) return strings.ToLower(code.String())
} }
func docTypeLabel(docType documentsv1.DocumentType) string { func docKindLabel(documentKind string) string {
label := docType.String() label := strings.TrimSpace(documentKind)
if label == "" { if label == "" {
return "DOCUMENT_TYPE_UNSPECIFIED" return "operation"
} }
return label return label

View File

@@ -4,13 +4,12 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"path/filepath"
"strings" "strings"
"time" "time"
"github.com/tech/sendico/billing/documents/internal/appversion" "github.com/tech/sendico/billing/documents/internal/appversion"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/internal/docstore" "github.com/tech/sendico/billing/documents/internal/docstore"
"github.com/tech/sendico/billing/documents/renderer" "github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage" "github.com/tech/sendico/billing/documents/storage"
@@ -146,131 +145,30 @@ func (s *Service) Shutdown() {
} }
} }
func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) { func (s *Service) GetOperationDocument(_ context.Context, req *documentsv1.GetOperationDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
start := time.Now() start := time.Now()
organizationRef := ""
var paymentRefs []string gatewayService := ""
if req != nil { operationRef := ""
paymentRefs = req.GetPaymentRefs()
}
logger := s.logger.With(zap.Int("payment_refs", len(paymentRefs)))
defer func() {
statusLabel := statusFromError(err)
observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start))
observeBatchSize(len(paymentRefs))
itemsCount := 0
if resp != nil {
itemsCount = len(resp.GetItems())
}
fields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Int("items", itemsCount),
}
if err != nil {
logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...)
return
}
logger.Info("BatchResolveDocuments finished", fields...)
}()
if len(paymentRefs) == 0 {
resp = &documentsv1.BatchResolveDocumentsResponse{}
return resp, nil
}
if s.storage == nil {
err = status.Error(codes.Unavailable, errStorageUnavailable.Error())
return nil, err
}
refs := make([]string, 0, len(paymentRefs))
for _, ref := range paymentRefs {
clean := strings.TrimSpace(ref)
if clean == "" {
continue
}
refs = append(refs, clean)
}
if len(refs) == 0 {
resp = &documentsv1.BatchResolveDocumentsResponse{}
return resp, nil
}
records, err := s.storage.Documents().ListByPaymentRefs(ctx, refs)
if err != nil {
return nil, status.Error(codes.Internal, err.Error())
}
recordByRef := map[string]*model.DocumentRecord{}
for _, record := range records {
if record == nil {
continue
}
recordByRef[record.PaymentRef] = record
}
items := make([]*documentsv1.DocumentMeta, 0, len(refs))
for _, ref := range refs {
meta := &documentsv1.DocumentMeta{PaymentRef: ref}
if record := recordByRef[ref]; record != nil {
record.Normalize()
available := []model.DocumentType{model.DocumentTypeAct}
ready := make([]model.DocumentType, 0, 1)
if path, ok := record.StoragePaths[model.DocumentTypeAct]; ok && path != "" {
ready = append(ready, model.DocumentTypeAct)
}
meta.AvailableTypes = toProtoTypes(available)
meta.ReadyTypes = toProtoTypes(ready)
}
items = append(items, meta)
}
resp = &documentsv1.BatchResolveDocumentsResponse{Items: items}
return resp, nil
}
func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
start := time.Now()
docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
paymentRef := ""
if req != nil { if req != nil {
docType = req.GetType() organizationRef = strings.TrimSpace(req.GetOrganizationRef())
paymentRef = strings.TrimSpace(req.GetPaymentRef()) gatewayService = strings.TrimSpace(req.GetGatewayService())
operationRef = strings.TrimSpace(req.GetOperationRef())
} }
logger := s.logger.With( logger := s.logger.With(
zap.String("payment_ref", paymentRef), zap.String("organization_ref", organizationRef),
zap.String("document_type", docTypeLabel(docType)), zap.String("gateway_service", gatewayService),
zap.String("operation_ref", operationRef),
) )
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
observeRequest("get_document", docType, statusLabel, time.Since(start)) observeRequest("get_operation_document", "operation", statusLabel, time.Since(start))
if resp != nil { if resp != nil {
observeDocumentBytes(docType, len(resp.GetContent())) observeDocumentBytes("operation", len(resp.GetContent()))
} }
contentBytes := 0 contentBytes := 0
@@ -285,100 +183,49 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
} }
if err != nil { if err != nil {
logger.Warn("GetDocument failed", append(fields, zap.Error(err))...) logger.Warn("GetOperationDocument failed", append(fields, zap.Error(err))...)
return return
} }
logger.Info("GetDocument finished", fields...) logger.Info("GetOperationDocument finished", fields...)
}() }()
if paymentRef == "" { if req == nil {
err = status.Error(codes.InvalidArgument, "payment_ref is required") err = status.Error(codes.InvalidArgument, "request is required")
return nil, err return nil, err
} }
if docType == documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED { if organizationRef == "" {
err = status.Error(codes.InvalidArgument, "document type is required") err = status.Error(codes.InvalidArgument, "organization_ref is required")
return nil, err return nil, err
} }
if s.storage == nil { if gatewayService == "" {
err = status.Error(codes.Unavailable, errStorageUnavailable.Error()) err = status.Error(codes.InvalidArgument, "gateway_service is required")
return nil, err return nil, err
} }
if s.docStore == nil { if operationRef == "" {
err = status.Error(codes.Unavailable, errDocStoreUnavailable.Error()) err = status.Error(codes.InvalidArgument, "operation_ref is required")
return nil, err return nil, err
} }
if s.template == nil { snapshot := operationSnapshotFromRequest(req)
err = status.Error(codes.FailedPrecondition, errTemplateUnavailable.Error()) content, _, genErr := s.generateOperationPDF(snapshot)
return nil, err
}
record, err := s.storage.Documents().GetByPaymentRef(ctx, paymentRef)
if err != nil {
if errors.Is(err, storage.ErrDocumentNotFound) {
return nil, status.Error(codes.NotFound, "document record not found")
}
return nil, status.Error(codes.Internal, err.Error())
}
record.Normalize()
targetType := model.DocumentTypeFromProto(docType)
if docType != documentsv1.DocumentType_DOCUMENT_TYPE_ACT {
return nil, status.Error(codes.Unimplemented, "document type not implemented")
}
if path, ok := record.StoragePaths[targetType]; ok && path != "" {
content, loadErr := s.docStore.Load(ctx, path)
if loadErr != nil {
return nil, status.Error(codes.Internal, loadErr.Error())
}
return &documentsv1.GetDocumentResponse{
Content: content,
Filename: documentFilename(docType, paymentRef),
MimeType: "application/pdf",
}, nil
}
content, hash, genErr := s.generateActPDF(record.Snapshot)
if genErr != nil { if genErr != nil {
logger.Warn("Failed to generate document", zap.Error(genErr)) err = status.Error(codes.Internal, genErr.Error())
return nil, status.Error(codes.Internal, genErr.Error()) return nil, err
}
path := documentStoragePath(paymentRef, docType)
if saveErr := s.docStore.Save(ctx, path, content); saveErr != nil {
logger.Warn("Failed to store document", zap.Error(saveErr))
return nil, status.Error(codes.Internal, saveErr.Error())
}
record.StoragePaths[targetType] = path
record.Hashes[targetType] = hash
if updateErr := s.storage.Documents().Update(ctx, record); updateErr != nil {
logger.Warn("Failed to update document record", zap.Error(updateErr))
return nil, status.Error(codes.Internal, updateErr.Error())
} }
resp = &documentsv1.GetDocumentResponse{ resp = &documentsv1.GetDocumentResponse{
Content: content, Content: content,
Filename: documentFilename(docType, paymentRef), Filename: operationDocumentFilename(operationRef),
MimeType: "application/pdf", MimeType: "application/pdf",
} }
@@ -391,8 +238,8 @@ func (s *Service) startDiscoveryAnnouncer() {
} }
announce := discovery.Announcement{ announce := discovery.Announcement{
Service: "BILLING_DOCUMENTS", Service: mservice.BillingDocuments,
Operations: []string{discovery.OperationDocumentsBatchResolve, discovery.OperationDocumentsGet}, Operations: []string{discovery.OperationDocumentsGet},
InvokeURI: s.invokeURI, InvokeURI: s.invokeURI,
Version: appversion.Create().Short(), Version: appversion.Create().Short(),
} }
@@ -400,28 +247,25 @@ func (s *Service) startDiscoveryAnnouncer() {
s.announcer.Start() s.announcer.Start()
} }
type serviceError string
func (e serviceError) Error() string {
return string(e)
}
var (
errStorageUnavailable = serviceError("documents: storage not initialised")
errDocStoreUnavailable = serviceError("documents: document store not initialised")
errTemplateUnavailable = serviceError("documents: template renderer not initialised")
)
func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) { func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, error) {
blocks, err := s.template.Render(snapshot) blocks, err := s.template.Render(snapshot)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return s.renderPDFWithIntegrity(blocks)
}
func (s *Service) generateOperationPDF(snapshot operationSnapshot) ([]byte, string, error) {
return s.renderPDFWithIntegrity(buildOperationBlocks(snapshot))
}
func (s *Service) renderPDFWithIntegrity(blocks []renderer.Block) ([]byte, string, error) {
generated := renderer.Renderer{ generated := renderer.Renderer{
Issuer: s.config.Issuer, Issuer: s.config.IssuerDetails(),
OwnerPassword: s.config.Protection.OwnerPassword, OwnerPassword: s.config.Protection.OwnerPassword,
} }
placeholder := strings.Repeat("0", 64) placeholder := strings.Repeat("0", 64)
firstPass, err := generated.Render(blocks, placeholder) firstPass, err := generated.Render(blocks, placeholder)
@@ -440,49 +284,303 @@ func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, er
return finalBytes, footerHex, nil return finalBytes, footerHex, nil
} }
func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType { type operationSnapshot struct {
if len(types) == 0 { OrganizationRef string
return nil GatewayService string
} OperationRef string
PaymentRef string
result := make([]documentsv1.DocumentType, 0, len(types)) ClientName string
for _, t := range types { ClientAddress string
result = append(result, t.Proto()) OperationCode string
} OperationLabel string
OperationState string
return result FailureCode string
FailureReason string
Amount string
Currency string
StartedAt time.Time
CompletedAt time.Time
} }
func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string { func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest) operationSnapshot {
suffix := "document.pdf" snapshot := operationSnapshot{
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
switch docType { GatewayService: strings.TrimSpace(req.GetGatewayService()),
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT: OperationRef: strings.TrimSpace(req.GetOperationRef()),
suffix = "act.pdf" PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE: ClientName: strings.TrimSpace(req.GetClientName()),
suffix = "invoice.pdf" ClientAddress: strings.TrimSpace(req.GetClientAddress()),
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT: OperationCode: strings.TrimSpace(req.GetOperationCode()),
suffix = "receipt.pdf" OperationLabel: strings.TrimSpace(req.GetOperationLabel()),
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED: OperationState: strings.TrimSpace(req.GetOperationState()),
// default suffix used FailureCode: strings.TrimSpace(req.GetFailureCode()),
FailureReason: strings.TrimSpace(req.GetFailureReason()),
Amount: strings.TrimSpace(req.GetAmount()),
Currency: strings.TrimSpace(req.GetCurrency()),
} }
return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix)) if ts := req.GetStartedAtUnixMs(); ts > 0 {
} snapshot.StartedAt = time.UnixMilli(ts).UTC()
}
func documentFilename(docType documentsv1.DocumentType, paymentRef string) string { if ts := req.GetCompletedAtUnixMs(); ts > 0 {
name := "document" snapshot.CompletedAt = time.UnixMilli(ts).UTC()
switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
name = "act"
case documentsv1.DocumentType_DOCUMENT_TYPE_INVOICE:
name = "invoice"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
name = "receipt"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default name used
} }
return fmt.Sprintf("%s_%s.pdf", name, paymentRef) return snapshot
}
func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
documentCopy := content.OperationDocument
blocks := []renderer.Block{
{
Tag: renderer.TagTitle,
Lines: []string{documentCopy.Title},
},
{
Tag: renderer.TagSubtitle,
Lines: []string{documentCopy.Subtitle},
},
{
Tag: renderer.TagMeta,
Lines: []string{
fmt.Sprintf("%s: %s", documentCopy.MetaCertificateNumberLabel, certificateNumber(snapshot)),
fmt.Sprintf("%s: %s", documentCopy.MetaDateLabel, formatCertificateDate(certificateDate(snapshot))),
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionParties},
},
{
Tag: renderer.TagText,
Lines: []string{documentCopy.PartiesIntro},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowServiceProvider, content.IssuerLegalName},
{documentCopy.RowServiceProviderAddress, content.IssuerLegalAddress},
{documentCopy.RowServiceProviderEmail, content.IssuerEmail},
{documentCopy.RowClient, certificateClientName(snapshot)},
{documentCopy.RowClientAddress, certificateClientAddress(snapshot)},
{documentCopy.RowClientReference, safeValue(snapshot.PaymentRef)},
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionSubject},
},
{
Tag: renderer.TagText,
Lines: []string{
documentCopy.SubjectIntro,
"",
"- Payment execution and orchestration services for payment reference " + safeValue(snapshot.PaymentRef) + ".",
"- Gateway service: " + safeValue(snapshot.GatewayService) + ".",
"- Operation reference: " + safeValue(snapshot.OperationRef) + ".",
"- Operation descriptor: " + operationDescriptor(snapshot) + ".",
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionServicePeriod},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowPeriodFrom, formatSnapshotTime(snapshot.StartedAt)},
{documentCopy.RowPeriodTo, formatSnapshotTime(snapshot.CompletedAt)},
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionTotalAmount},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowTotalAmount, operationAmount(snapshot)},
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionClientConfirmation},
},
{
Tag: renderer.TagText,
Lines: []string{
documentCopy.ConfirmationLine1,
documentCopy.ConfirmationLine2,
documentCopy.ConfirmationLine3,
"",
"This Certificate serves as confirmation of the completion and acceptance of the services.",
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionSignatures},
},
{
Tag: renderer.TagSign,
Lines: []string{
documentCopy.SignatureServiceProviderLine,
"",
documentCopy.SignatureClientNamePrefix + " " + certificateClientName(snapshot),
documentCopy.SignatureClientTitleLine,
documentCopy.SignatureClientLine,
},
},
{
Tag: renderer.TagSection,
Lines: []string{documentCopy.SectionOperationStatus},
},
{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowOperationStatus, safeValue(snapshot.OperationState)},
{documentCopy.RowOperationCode, safeValue(snapshot.OperationCode)},
{documentCopy.RowOperationLabel, safeValue(snapshot.OperationLabel)},
},
},
}
if snapshot.FailureCode != "" || snapshot.FailureReason != "" {
blocks = append(blocks, renderer.Block{
Tag: renderer.TagKV,
Rows: [][]string{
{documentCopy.RowFailureCode, safeValue(snapshot.FailureCode)},
{documentCopy.RowFailureReason, safeValue(snapshot.FailureReason)},
},
})
}
return blocks
}
func formatSnapshotTime(value time.Time) string {
if value.IsZero() {
return content.OperationDocument.MissingValuePlaceholder
}
return value.UTC().Format(time.RFC3339)
}
func certificateNumber(snapshot operationSnapshot) string {
if paymentRef := strings.TrimSpace(snapshot.PaymentRef); paymentRef != "" {
return paymentRef
}
if operationRef := strings.TrimSpace(snapshot.OperationRef); operationRef != "" {
return operationRef
}
return content.OperationDocument.MissingValuePlaceholder
}
func certificateDate(snapshot operationSnapshot) time.Time {
if !snapshot.CompletedAt.IsZero() {
return snapshot.CompletedAt.UTC()
}
if !snapshot.StartedAt.IsZero() {
return snapshot.StartedAt.UTC()
}
return time.Now().UTC()
}
func formatCertificateDate(value time.Time) string {
if value.IsZero() {
return content.OperationDocument.MissingValuePlaceholder
}
return value.UTC().Format("January 2, 2006")
}
func operationAmount(snapshot operationSnapshot) string {
amount := strings.TrimSpace(snapshot.Amount)
currency := strings.TrimSpace(snapshot.Currency)
if amount == "" && currency == "" {
return content.OperationDocument.MissingValuePlaceholder
}
return strings.TrimSpace(amount + " " + currency)
}
func operationDescriptor(snapshot operationSnapshot) string {
label := strings.TrimSpace(snapshot.OperationLabel)
code := strings.TrimSpace(snapshot.OperationCode)
switch {
case label != "" && code != "":
return fmt.Sprintf("%s (%s)", label, code)
case label != "":
return label
case code != "":
return code
default:
return content.OperationDocument.MissingValuePlaceholder
}
}
func certificateClientName(snapshot operationSnapshot) string {
if name := strings.TrimSpace(snapshot.ClientName); name != "" {
return name
}
return "John Doe"
}
func certificateClientAddress(snapshot operationSnapshot) string {
if address := strings.TrimSpace(snapshot.ClientAddress); address != "" {
return address
}
return content.OperationDocument.MissingValuePlaceholder
}
func safeValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return content.OperationDocument.MissingValuePlaceholder
}
return trimmed
}
func operationDocumentFilename(operationRef string) string {
clean := sanitizeFilenameComponent(operationRef)
if clean == "" {
clean = "operation"
}
return fmt.Sprintf("operation_%s.pdf", clean)
}
func sanitizeFilenameComponent(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
var b strings.Builder
b.Grow(len(trimmed))
for _, r := range trimmed {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= 'A' && r <= 'Z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-', r == '_':
b.WriteRune(r)
default:
b.WriteRune('_')
}
}
return strings.Trim(b.String(), "_")
} }

View File

@@ -3,15 +3,19 @@ package documents
import ( import (
"bytes" "bytes"
"context" "context"
"strings"
"testing" "testing"
"time" "time"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/renderer" "github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage" "github.com/tech/sendico/billing/documents/storage"
"github.com/tech/sendico/billing/documents/storage/model" "github.com/tech/sendico/billing/documents/storage/model"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1" documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
"go.uber.org/zap" "go.uber.org/zap"
"google.golang.org/grpc/codes"
"google.golang.org/grpc/status"
) )
type stubRepo struct { type stubRepo struct {
@@ -51,38 +55,6 @@ func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([
var _ storage.DocumentsStore = (*stubDocumentsStore)(nil) var _ storage.DocumentsStore = (*stubDocumentsStore)(nil)
type memDocStore struct {
data map[string][]byte
saveCount int
loadCount int
}
func newMemDocStore() *memDocStore {
return &memDocStore{data: map[string][]byte{}}
}
func (m *memDocStore) Save(_ context.Context, key string, data []byte) error {
m.saveCount++
copyData := make([]byte, len(data))
copy(copyData, data)
m.data[key] = copyData
return nil
}
func (m *memDocStore) Load(_ context.Context, key string) ([]byte, error) {
m.loadCount++
data := m.data[key]
copyData := make([]byte, len(data))
copy(copyData, data)
return copyData, nil
}
func (m *memDocStore) Counts() (int, int) {
return m.saveCount, m.loadCount
}
type stubTemplate struct { type stubTemplate struct {
blocks []renderer.Block blocks []renderer.Block
calls int calls int
@@ -94,9 +66,7 @@ func (s *stubTemplate) Render(_ model.ActSnapshot) ([]renderer.Block, error) {
return s.blocks, nil return s.blocks, nil
} }
func TestGetDocument_IdempotentAndHashed(t *testing.T) { func TestGenerateActPDF_IdempotentAndHashed(t *testing.T) {
ctx := context.Background()
snapshot := model.ActSnapshot{ snapshot := model.ActSnapshot{
PaymentID: "PAY-123", PaymentID: "PAY-123",
Date: time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC), Date: time.Date(2026, 1, 30, 0, 0, 0, 0, time.UTC),
@@ -105,14 +75,6 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
Currency: "USD", Currency: "USD",
} }
record := &model.DocumentRecord{
PaymentRef: "PAY-123",
Snapshot: snapshot,
}
documentsStore := &stubDocumentsStore{record: record}
repo := &stubRepo{store: documentsStore}
store := newMemDocStore()
tmpl := &stubTemplate{ tmpl := &stubTemplate{
blocks: []renderer.Block{ blocks: []renderer.Block{
{Tag: renderer.TagTitle, Lines: []string{"ACT"}}, {Tag: renderer.TagTitle, Lines: []string{"ACT"}},
@@ -120,74 +82,51 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
}, },
} }
cfg := Config{ svc := NewService(zap.NewNop(), nil, nil,
Issuer: renderer.Issuer{
LegalName: "Sendico Ltd",
LegalAddress: "12 Market Street, London, UK",
},
}
svc := NewService(zap.NewNop(), repo, nil,
WithConfig(cfg),
WithDocumentStore(store),
WithTemplateRenderer(tmpl), WithTemplateRenderer(tmpl),
) )
resp1, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{ pdf1, hash1, err := svc.generateActPDF(snapshot)
PaymentRef: "PAY-123",
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
})
if err != nil { 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") t.Fatalf("expected content on first call")
} }
stored := record.Hashes[model.DocumentTypeAct] if hash1 == "" {
t.Fatalf("expected non-empty hash on first call")
if stored == "" {
t.Fatalf("expected stored hash")
} }
footerHash := extractFooterHash(resp1.GetContent()) footerHash := extractFooterHash(pdf1)
if footerHash == "" { if footerHash == "" {
t.Fatalf("expected footer hash in PDF") t.Fatalf("expected footer hash in PDF")
} }
if stored != footerHash { if hash1 != footerHash {
t.Fatalf("stored hash mismatch: got %s", stored) t.Fatalf("stored hash mismatch: got %s", hash1)
} }
resp2, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{ pdf2, hash2, err := svc.generateActPDF(snapshot)
PaymentRef: "PAY-123",
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
})
if err != nil { if err != nil {
t.Fatalf("GetDocument second call: %v", err) t.Fatalf("generateActPDF second call: %v", err)
} }
if hash2 == "" {
if !bytes.Equal(resp1.GetContent(), resp2.GetContent()) { t.Fatalf("expected non-empty hash on second call")
t.Fatalf("expected identical PDF bytes on second call")
} }
footerHash2 := extractFooterHash(pdf2)
if tmpl.calls != 1 { if footerHash2 == "" {
t.Fatalf("expected template to be rendered once, got %d", tmpl.calls) t.Fatalf("expected footer hash in second PDF")
} }
if footerHash2 != hash2 {
if store.saveCount != 1 { t.Fatalf("second hash mismatch: got=%s want=%s", footerHash2, hash2)
t.Fatalf("expected document save once, got %d", store.saveCount)
}
if store.loadCount == 0 {
t.Fatalf("expected document load on second call")
} }
} }
func extractFooterHash(pdf []byte) string { func extractFooterHash(pdf []byte) string {
prefix := []byte("Document integrity hash: ") prefix := []byte(content.DocumentIntegrityHashPrefix)
idx := bytes.Index(pdf, prefix) idx := bytes.Index(pdf, prefix)
if idx == -1 { if idx == -1 {
@@ -212,3 +151,133 @@ func extractFooterHash(pdf []byte) string {
func isHexDigit(b byte) bool { func isHexDigit(b byte) bool {
return (b >= '0' && b <= '9') || (b >= 'a' && b <= 'f') || (b >= 'A' && b <= 'F') 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)
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)
}
}
func TestBuildOperationBlocks_CertificateIncludesPaymentData(t *testing.T) {
snapshot := operationSnapshot{
OrganizationRef: "org-1",
GatewayService: "chain_gateway",
OperationRef: "op-123",
PaymentRef: "pay-123",
ClientName: "Jane Customer",
ClientAddress: "Main Street 1, City",
OperationCode: "transfer",
OperationLabel: "Outbound transfer",
OperationState: "completed",
Amount: "100.50",
Currency: "USDT",
StartedAt: time.Date(2026, 3, 1, 10, 0, 0, 0, time.UTC),
CompletedAt: time.Date(2026, 3, 2, 12, 0, 0, 0, time.UTC),
}
blocks := buildOperationBlocks(snapshot)
if len(blocks) == 0 {
t.Fatalf("expected blocks")
}
if got := blocks[0].Lines[0]; got != content.OperationDocument.Title {
t.Fatalf("title mismatch: got=%q want=%q", got, content.OperationDocument.Title)
}
meta := findTaggedBlock(blocks, renderer.TagMeta)
if meta == nil {
t.Fatalf("expected meta block")
}
metaText := strings.Join(meta.Lines, "\n")
if !strings.Contains(metaText, "Certificate No.: pay-123") {
t.Fatalf("meta should include certificate number, got=%q", metaText)
}
if !strings.Contains(metaText, "Date: March 2, 2026") {
t.Fatalf("meta should include certificate date, got=%q", metaText)
}
amountFound := false
clientFound := false
for _, block := range blocks {
if block.Tag != renderer.TagKV {
continue
}
for _, row := range block.Rows {
if len(row) >= 2 && row[0] == content.OperationDocument.RowTotalAmount && row[1] == "100.50 USDT" {
amountFound = true
}
if len(row) >= 2 && row[0] == content.OperationDocument.RowClient && row[1] == "Jane Customer" {
clientFound = true
}
}
}
if !amountFound {
t.Fatalf("expected total amount row with payment amount")
}
if !clientFound {
t.Fatalf("expected client row with customer name")
}
}
func TestCertificateNumber_FallsBackToOperationRef(t *testing.T) {
got := certificateNumber(operationSnapshot{
OperationRef: "op-777",
})
if got != "op-777" {
t.Fatalf("certificateNumber fallback mismatch: got=%q want=%q", got, "op-777")
}
}
func TestCertificateClientName_Fallback(t *testing.T) {
if got := certificateClientName(operationSnapshot{}); got != "John Doe" {
t.Fatalf("certificateClientName fallback mismatch: got=%q want=%q", got, "John Doe")
}
}
func findTaggedBlock(blocks []renderer.Block, tag renderer.Tag) *renderer.Block {
for i := range blocks {
if blocks[i].Tag == tag {
return &blocks[i]
}
}
return nil
}

View File

@@ -9,6 +9,7 @@ import (
"time" "time"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/renderer" "github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage/model" "github.com/tech/sendico/billing/documents/storage/model"
) )
@@ -17,7 +18,13 @@ type templateRenderer struct {
tpl *template.Template tpl *template.Template
} }
type acceptanceTemplateData struct {
model.ActSnapshot
Content content.AcceptanceTemplateContent
}
func newTemplateRenderer(path string) (*templateRenderer, error) { func newTemplateRenderer(path string) (*templateRenderer, error) {
//nolint:gosec // template file path is provided by trusted service configuration.
data, err := os.ReadFile(path) data, err := os.ReadFile(path)
if err != nil { if err != nil {
return nil, fmt.Errorf("read template: %w", err) return nil, fmt.Errorf("read template: %w", err)
@@ -38,7 +45,12 @@ func newTemplateRenderer(path string) (*templateRenderer, error) {
func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) { func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) {
var buf bytes.Buffer var buf bytes.Buffer
if err := r.tpl.Execute(&buf, snapshot); err != nil { data := acceptanceTemplateData{
ActSnapshot: snapshot,
Content: content.AcceptanceTemplate,
}
if err := r.tpl.Execute(&buf, data); err != nil {
return nil, fmt.Errorf("execute template: %w", err) return nil, fmt.Errorf("execute template: %w", err)
} }

View File

@@ -7,6 +7,7 @@ import (
"time" "time"
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tech/sendico/billing/documents/internal/content"
"github.com/tech/sendico/billing/documents/renderer" "github.com/tech/sendico/billing/documents/renderer"
"github.com/tech/sendico/billing/documents/storage/model" "github.com/tech/sendico/billing/documents/storage/model"
) )
@@ -42,7 +43,7 @@ func TestTemplateRenderer_Render(t *testing.T) {
t.Fatalf("expected title block") t.Fatalf("expected title block")
} }
if !slices.Contains(title.Lines, "ACT OF ACCEPTANCE OF SERVICES") { if !slices.Contains(title.Lines, content.AcceptanceTemplate.Title) {
t.Fatalf("expected title content not found") t.Fatalf("expected title content not found")
} }
@@ -54,7 +55,7 @@ func TestTemplateRenderer_Render(t *testing.T) {
foundExecutor := false foundExecutor := false
for _, row := range kv.Rows { for _, row := range kv.Rows {
if len(row) >= 2 && row[0] == "Executor" && row[1] == snapshot.ExecutorFullName { if len(row) >= 2 && row[0] == content.AcceptanceTemplate.PartyExecutorLabel && row[1] == snapshot.ExecutorFullName {
foundExecutor = true foundExecutor = true
break break

View File

@@ -6,6 +6,7 @@ import (
"strings" "strings"
"github.com/jung-kurt/gofpdf" "github.com/jung-kurt/gofpdf"
"github.com/tech/sendico/billing/documents/internal/content"
) )
const ( const (
@@ -28,7 +29,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetAutoPageBreak(true, pageMarginBottom) pdf.SetAutoPageBreak(true, pageMarginBottom)
pdf.SetCompression(false) pdf.SetCompression(false)
pdf.SetAuthor(r.Issuer.LegalName, false) pdf.SetAuthor(r.Issuer.LegalName, false)
pdf.SetTitle("Act of Acceptance", false) pdf.SetTitle(content.PDFTitleActOfAcceptance, false)
owner := strings.TrimSpace(r.OwnerPassword) owner := strings.TrimSpace(r.OwnerPassword)
if owner != "" { if owner != "" {
@@ -39,7 +40,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetY(-15) pdf.SetY(-15)
pdf.SetFont("Helvetica", "", 8) pdf.SetFont("Helvetica", "", 8)
footer := "Document integrity hash: " + footerHash footer := content.DocumentIntegrityHashPrefix + footerHash
pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "") pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
}) })

View File

@@ -6,6 +6,8 @@ import (
"strings" "strings"
"testing" "testing"
"unicode/utf16" "unicode/utf16"
"github.com/tech/sendico/billing/documents/internal/content"
) )
func TestRenderer_RenderContainsText(t *testing.T) { func TestRenderer_RenderContainsText(t *testing.T) {
@@ -31,7 +33,7 @@ func TestRenderer_RenderContainsText(t *testing.T) {
t.Fatalf("expected PDF bytes") t.Fatalf("expected PDF bytes")
} }
checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", "Document integrity hash"} checks := []string{"Sendico Ltd", "Jane Doe", "100 USD", strings.TrimSpace(strings.TrimSuffix(content.DocumentIntegrityHashPrefix, ": "))}
for _, token := range checks { for _, token := range checks {
if !containsPDFText(pdfBytes, token) { if !containsPDFText(pdfBytes, token) {
@@ -100,7 +102,7 @@ func encodeUTF16BE(text string, withBOM bool) []byte {
} }
for _, v := range encoded { for _, v := range encoded {
out = append(out, byte(v>>8), byte(v)) out = append(out, byte(v>>8), byte(v&0x00FF))
} }
return out return out

View File

@@ -6,14 +6,13 @@ import (
"github.com/shopspring/decimal" "github.com/shopspring/decimal"
"github.com/tech/sendico/pkg/db/storable" "github.com/tech/sendico/pkg/db/storable"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
) )
const ( const (
DocumentRecordsCollection = "document_records" DocumentRecordsCollection = "document_records"
) )
// DocumentType mirrors the protobuf enum but stores string names for Mongo compatibility. // DocumentType represents document kinds cached in storage.
type DocumentType string type DocumentType string
const ( const (
@@ -23,24 +22,6 @@ const (
DocumentTypeReceipt DocumentType = "DOCUMENT_TYPE_RECEIPT" DocumentTypeReceipt DocumentType = "DOCUMENT_TYPE_RECEIPT"
) )
// DocumentTypeFromProto converts a protobuf enum to the storage representation.
func DocumentTypeFromProto(t documentsv1.DocumentType) DocumentType {
if name, ok := documentsv1.DocumentType_name[int32(t)]; ok {
return DocumentType(name)
}
return DocumentTypeUnspecified
}
// Proto converts the storage representation to a protobuf enum.
func (t DocumentType) Proto() documentsv1.DocumentType {
if value, ok := documentsv1.DocumentType_value[string(t)]; ok {
return documentsv1.DocumentType(value)
}
return documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
}
// ActSnapshot captures the immutable data needed to generate an acceptance act. // ActSnapshot captures the immutable data needed to generate an acceptance act.
type ActSnapshot struct { type ActSnapshot struct {
PaymentID string `bson:"paymentId" json:"paymentId"` PaymentID string `bson:"paymentId" json:"paymentId"`

View File

@@ -2,66 +2,66 @@
#title #title
ACT OF ACCEPTANCE OF SERVICES {{ .Content.Title }}
#subtitle #subtitle
under the Public Offer Agreement {{ .Content.Subtitle }}
#meta #meta
Date: {{ date .Date }} {{ .Content.MetaDateLabel }}: {{ date .Date }}
Act No: {{ .PaymentID }} {{ .Content.MetaActNumberLabel }}: {{ .PaymentID }}
#section #section
PARTIES {{ .Content.SectionParties }}
#text #text
This Act is made between the following Parties. {{ .Content.PartiesIntro }}
#kv #kv
Executor | {{ .ExecutorFullName }} {{ .Content.PartyExecutorLabel }} | {{ .ExecutorFullName }}
Status | Individual {{ .Content.PartyStatusLabel }} | {{ .Content.PartyStatusValue }}
#section #section
BASIS {{ .Content.SectionBasis }}
#text #text
This Act is issued pursuant to the Public Offer Agreement {{ .Content.BasisLine1 }}
accepted by the Executor by joining the offer. {{ .Content.BasisLine2 }}
#section #section
SERVICES RENDERED {{ .Content.SectionServicesRendered }}
#text #text
The Executor has rendered services to the Customer {{ .Content.ServicesRenderedLine1 }}
in accordance with the terms of the Public Offer Agreement. {{ .Content.ServicesRenderedLine2 }}
#section #section
REMUNERATION {{ .Content.SectionRemuneration }}
#table #table
Description | Amount {{ .Content.RemunerationHeaderDesc }} | {{ .Content.RemunerationHeaderAmount }}
Services rendered under the Public Offer Agreement | {{ money .Amount .Currency }} {{ .Content.RemunerationServicesDesc }} | {{ money .Amount .Currency }}
#section #section
CONFIRMATION {{ .Content.SectionConfirmation }}
#text #text
The Customer confirms that the services were rendered properly {{ .Content.ConfirmationLine1 }}
and accepted without any claims. {{ .Content.ConfirmationLine2 }}
The remuneration for the services was paid to the Executor {{ .Content.ConfirmationPaymentLine1 }}
using the bank card details provided by the Executor. {{ .Content.ConfirmationPaymentLine2 }}
#section #section
SIGNATURES {{ .Content.SectionSignatures }}
#sign #sign
Customer ___________________________ {{ .Content.SignatureCustomerLine }}
Executor ___________________________ {{ .Content.SignatureExecutorLine }}

View File

@@ -1,198 +1,47 @@
# See the dedicated "version" documentation section.
version: "2" version: "2"
linters: linters:
# Default set of linters. default: none
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable: enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose - bodyclose
- canonicalheader - canonicalheader
- containedctx
- contextcheck
- copyloopvar - copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck - durationcheck
- embeddedstructfieldcheck
- err113
- errcheck - errcheck
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec - gosec
- gosmopolitan
- govet - govet
- grouper
- iface
- importas
- inamedparam
- ineffassign - ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr - nilerr
- nilnesserr - nilnesserr
- nilnil - nilnil
- nlreturn
- noctx - noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck - rowserrcheck
- sloglint
- spancheck
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert - unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign - wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable: disable:
- depguard - depguard
- exhaustruct - exhaustruct
- gochecknoglobals - gochecknoglobals
- gochecknoinits
- gomoddirectives - gomoddirectives
- noinlineerr
- wsl
- wrapcheck - wrapcheck
# All available settings of specific linters. - cyclop
# See the dedicated "linters.settings" documentation section. - dupl
settings: - funlen
wsl_v5: - gocognit
allow-first-in-block: true - gocyclo
allow-whole-block: false - ireturn
branch-max-lines: 2 - lll
- mnd
# Defines a set of rules to ignore issues. - nestif
# It does not skip the analysis, and so does not ignore "typecheck" errors. - nlreturn
exclusions: - noinlineerr
# Mode of the generated files analysis. - paralleltest
# - tagliatelle
# - `strict`: sources are excluded by strictly following the Go generated file convention. - testpackage
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - varnamelen
# This line must appear before the first non-comment, non-blank text in the file. - wsl_v5
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- cyclop
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -10,7 +10,7 @@ require (
github.com/tech/sendico/fx/oracle v0.0.0 github.com/tech/sendico/fx/oracle v0.0.0
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.79.2
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -44,12 +44,12 @@ require (
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
) )

View File

@@ -168,39 +168,39 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,10 +208,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -21,6 +21,7 @@ import (
msg "github.com/tech/sendico/pkg/messaging" msg "github.com/tech/sendico/pkg/messaging"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@@ -385,7 +386,7 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)} logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)}
if !orgRef.IsZero() { if !orgRef.IsZero() {
logFields = append(logFields, zap.String("organization_ref", orgRef.Hex())) logFields = append(logFields, mzap.ObjRef("organization_ref", orgRef))
} }
logFields = append(logFields, logFieldsFromIntent(intent)...) logFields = append(logFields, logFieldsFromIntent(intent)...)
@@ -563,7 +564,7 @@ func (s *Service) startDiscoveryAnnouncer() {
} }
announce := discovery.Announcement{ announce := discovery.Announcement{
Service: "BILLING_FEES", Service: mservice.BillingFees,
Operations: []string{discovery.OperationFeeCalc}, Operations: []string{discovery.OperationFeeCalc},
InvokeURI: s.invokeURI, InvokeURI: s.invokeURI,
Version: appversion.Create().Short(), Version: appversion.Create().Short(),

View File

@@ -1,196 +1,47 @@
# See the dedicated "version" documentation section.
version: "2" version: "2"
linters: linters:
# Default set of linters. default: none
# The value can be:
# - `standard`: https://golangci-lint.run/docs/linters/#enabled-by-default
# - `all`: enables all linters by default.
# - `none`: disables all linters by default.
# - `fast`: enables only linters considered as "fast" (`golangci-lint help linters --json | jq '[ .[] | select(.fast==true) ] | map(.name)'`).
# Default: standard
default: all
# Enable specific linter.
enable: enable:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose - bodyclose
- canonicalheader - canonicalheader
- containedctx
- contextcheck
- copyloopvar - copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck - durationcheck
- embeddedstructfieldcheck
- err113
- errcheck - errcheck
- errchkjson - errchkjson
- errname - errname
- errorlint - errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec - gosec
- gosmopolitan
- govet - govet
- grouper
- iface
- importas
- inamedparam
- ineffassign - ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr - nilerr
- nilnesserr - nilnesserr
- nilnil - nilnil
- nlreturn
- noctx - noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck - rowserrcheck
- sloglint
- spancheck
- sqlclosecheck - sqlclosecheck
- staticcheck - staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert - unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign - wastedassign
- whitespace disable:
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard - depguard
- exhaustruct - exhaustruct
- gochecknoglobals - gochecknoglobals
- gochecknoinits
- gomoddirectives - gomoddirectives
- wsl
- wrapcheck - wrapcheck
# All available settings of specific linters. - cyclop
# See the dedicated "linters.settings" documentation section. - dupl
settings: - funlen
wsl_v5: - gocognit
allow-first-in-block: true - gocyclo
allow-whole-block: false - ireturn
branch-max-lines: 2 - lll
- mnd
# Defines a set of rules to ignore issues. - nestif
# It does not skip the analysis, and so does not ignore "typecheck" errors. - nlreturn
exclusions: - noinlineerr
# Mode of the generated files analysis. - paralleltest
# - tagliatelle
# - `strict`: sources are excluded by strictly following the Go generated file convention. - testpackage
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$` - varnamelen
# This line must appear before the first non-comment, non-blank text in the file. - wsl_v5
# https://go.dev/s/generatedcode
# - `lax`: sources are excluded if they contain lines like `autogenerated file`, `code generated`, `do not edit`, etc.
# - `disable`: disable the generated files exclusion.
#
# Default: strict
generated: lax
# Log a warning if an exclusion rule is unused.
# Default: false
warn-unused: true
# Predefined exclusion rules.
# Default: []
presets:
- comments
- std-error-handling
- common-false-positives
- legacy
# Excluding configuration per-path, per-linter, per-text and per-source.
rules:
# Exclude some linters from running on tests files.
- path: _test\.go
linters:
- funlen
- gocyclo
- errcheck
- dupl
- gosec
# Run some linter only for test files by excluding its issues for everything else.
- path-except: _test\.go
linters:
- forbidigo
# Exclude known linters from partially hard-vendored code,
# which is impossible to exclude via `nolint` comments.
# `/` will be replaced by the current OS file path separator to properly work on Windows.
- path: internal/hmac/
text: "weak cryptographic primitive"
linters:
- gosec
# Exclude some `staticcheck` messages.
- linters:
- staticcheck
text: "SA9003:"
# Exclude `lll` issues for long lines with `go:generate`.
- linters:
- lll
source: "^//go:generate "
# Which file paths to exclude: they will be analyzed, but issues from them won't be reported.
# "/" will be replaced by the current OS file path separator to properly work on Windows.
# Default: []
paths: []
# Which file paths to not exclude.
# Default: []
paths-except: []

View File

@@ -37,13 +37,13 @@ require (
github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/net v0.51.0 // indirect golang.org/x/net v0.52.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
google.golang.org/grpc v1.79.1 // indirect google.golang.org/grpc v1.79.2 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -168,39 +168,39 @@ go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
@@ -208,10 +208,10 @@ golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -49,7 +49,7 @@ func (i *Imp) startDiscovery(cfg *config) error {
i.registrySvc = svc i.registrySvc = svc
announce := discovery.Announcement{ announce := discovery.Announcement{
Service: "DISCOVERY", Service: mservice.Discovery,
InstanceID: discovery.InstanceID(), InstanceID: discovery.InstanceID(),
Operations: []string{discovery.OperationDiscoveryLookup}, Operations: []string{discovery.OperationDiscoveryLookup},
Version: appversion.Create().Short(), Version: appversion.Create().Short(),

View File

@@ -0,0 +1,47 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- canonicalheader
- copyloopvar
- durationcheck
- errcheck
- errchkjson
- errname
- errorlint
- gosec
- govet
- ineffassign
- nilerr
- nilnesserr
- nilnil
- noctx
- rowserrcheck
- sqlclosecheck
- staticcheck
- unconvert
- wastedassign
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gochecknoinits
- gomoddirectives
- wrapcheck
- cyclop
- dupl
- funlen
- gocognit
- gocyclo
- ireturn
- lll
- mnd
- nestif
- nlreturn
- noinlineerr
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl_v5

View File

@@ -15,10 +15,10 @@ replace github.com/tech/sendico/payments/storage => ../../payments/storage
replace github.com/tech/sendico/gateway/tron => ../../gateway/tron replace github.com/tech/sendico/gateway/tron => ../../gateway/tron
require ( require (
github.com/aws/aws-sdk-go-v2 v1.41.2 github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.10 github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0
github.com/go-chi/chi/v5 v5.2.5 github.com/go-chi/chi/v5 v5.2.5
github.com/go-chi/cors v1.2.2 github.com/go-chi/cors v1.2.2
github.com/go-chi/jwtauth/v5 v5.4.0 github.com/go-chi/jwtauth/v5 v5.4.0
@@ -37,8 +37,8 @@ require (
github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0
go.mongodb.org/mongo-driver/v2 v2.5.0 go.mongodb.org/mongo-driver/v2 v2.5.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
golang.org/x/net v0.51.0 golang.org/x/net v0.52.0
google.golang.org/grpc v1.79.1 google.golang.org/grpc v1.79.2
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
moul.io/chizap v1.0.3 moul.io/chizap v1.0.3
@@ -54,20 +54,20 @@ require (
dario.cat/mergo v1.0.1 // indirect dario.cat/mergo v1.0.1 // indirect
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 // indirect
github.com/Microsoft/go-winio v0.6.2 // indirect github.com/Microsoft/go-winio v0.6.2 // indirect
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 // indirect github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 // indirect
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 // indirect github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 // indirect github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 // indirect
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 // indirect github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 // indirect
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 // indirect github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 // indirect github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 // indirect github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 // indirect
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 // indirect github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 // indirect
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 // indirect github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 // indirect
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 // indirect github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 // indirect
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.2 // indirect github.com/aws/smithy-go v1.24.2 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
@@ -88,7 +88,7 @@ require (
github.com/go-logr/logr v1.4.3 // indirect github.com/go-logr/logr v1.4.3 // indirect
github.com/go-logr/stdr v1.2.2 // indirect github.com/go-logr/stdr v1.2.2 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-json v0.10.6 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/hashicorp/errwrap v1.1.0 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/hashicorp/go-cleanhttp v0.5.2 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
@@ -147,17 +147,17 @@ require (
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect
go.opentelemetry.io/otel v1.40.0 // indirect go.opentelemetry.io/otel v1.42.0 // indirect
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/metric v1.40.0 // indirect go.opentelemetry.io/otel/metric v1.42.0 // indirect
go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.40.0 // indirect go.opentelemetry.io/otel/trace v1.42.0 // indirect
go.uber.org/multierr v1.11.0 // indirect go.uber.org/multierr v1.11.0 // indirect
go.yaml.in/yaml/v2 v2.4.3 // indirect go.yaml.in/yaml/v2 v2.4.4 // indirect
golang.org/x/crypto v0.48.0 // indirect golang.org/x/crypto v0.49.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.20.0 // indirect
golang.org/x/sys v0.41.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.34.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/time v0.14.0 // indirect golang.org/x/time v0.15.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c // indirect
) )

View File

@@ -6,42 +6,42 @@ github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25
github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E=
github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY=
github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU=
github.com/aws/aws-sdk-go-v2 v1.41.2 h1:LuT2rzqNQsauaGkPK/7813XxcZ3o3yePY0Iy891T2ls= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.2/go.mod h1:IvvlAZQXvTXznUPfRVfryiG1fbzE2NGK6m9u39YQ+S4= github.com/aws/aws-sdk-go-v2 v1.41.3/go.mod h1:mwsPRE8ceUUpiTgF7QmQIJ7lgsKUPQOUl3o72QBrE1o=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5 h1:zWFmPmgw4sveAYi1mRqG+E/g0461cJ5M4bJ8/nc6d3Q= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6 h1:N4lRUXZpZ1KVEUn6hxtco/1d2lgYhNn1fHkkl8WhlyQ=
github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.5/go.mod h1:nVUlMLVV8ycXSb7mSkcNu9e3v/1TJq2RTlrPwhYWr5c= github.com/aws/aws-sdk-go-v2/aws/protocol/eventstream v1.7.6/go.mod h1:lyw7GFp3qENLh7kwzf7iMzAxDn+NzjXEAGjKS2UOKqI=
github.com/aws/aws-sdk-go-v2/config v1.32.10 h1:9DMthfO6XWZYLfzZglAgW5Fyou2nRI5CuV44sTedKBI= github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.10/go.mod h1:2rUIOnA2JaiqYmSKYmRJlcMWy6qTj1vuRFscppSBMcw= github.com/aws/aws-sdk-go-v2/config v1.32.11/go.mod h1:twF11+6ps9aNRKEDimksp923o44w/Thk9+8YIlzWMmo=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10 h1:EEhmEUFCE1Yhl7vDhNOI5OCL/iKMdkkYFTRpZXNw7m8= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.10/go.mod h1:RnnlFCAlxQCkN2Q379B67USkBMu1PipEEiibzYN5UTE= github.com/aws/aws-sdk-go-v2/credentials v1.19.11/go.mod h1:30yY2zqkMPdrvxBqzI9xQCM+WrlrZKSOpSJEsylVU+8=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18 h1:Ii4s+Sq3yDfaMLpjrJsqD6SmG/Wq/P5L/hw2qa78UAY= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19 h1:INUvJxmhdEbVulJYHI061k4TVuS3jzzthNvjqvVvTKM=
github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.18/go.mod h1:6x81qnY++ovptLE6nWQeWrpXxbnlIex+4H4eYYGcqfc= github.com/aws/aws-sdk-go-v2/feature/ec2/imds v1.18.19/go.mod h1:FpZN2QISLdEBWkayloda+sZjVJL+e9Gl0k1SyTgcswU=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18 h1:F43zk1vemYIqPAwhjTjYIz0irU2EY7sOb/F5eJ3HuyM= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19 h1:/sECfyq2JTifMI2JPyZ4bdRN77zJmr6SrS1eL3augIA=
github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.18/go.mod h1:w1jdlZXrGKaJcNoL+Nnrj+k5wlpGXqnNrKoP22HvAug= github.com/aws/aws-sdk-go-v2/internal/configsources v1.4.19/go.mod h1:dMf8A5oAqr9/oxOfLkC/c2LU/uMcALP0Rgn2BD5LWn0=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18 h1:xCeWVjj0ki0l3nruoyP2slHsGArMxeiiaoPN5QZH6YQ= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19 h1:AWeJMk33GTBf6J20XJe6qZoRSJo0WfUhsMdUKhoODXE=
github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.18/go.mod h1:r/eLGuGCBw6l36ZRWiw6PaZwPXb6YOj+i/7MizNl5/k= github.com/aws/aws-sdk-go-v2/internal/endpoints/v2 v2.7.19/go.mod h1:+GWrYoaAsV7/4pNHpwh1kiNLXkKaSoppxQq9lbH8Ejw=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4 h1:WKuaxf++XKWlHWu9ECbMlha8WOEGm0OUEZqm4K/Gcfk= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5 h1:clHU5fm//kWS1C2HgtgWxfQbFbx4b6rx+5jzhgX9HrI=
github.com/aws/aws-sdk-go-v2/internal/ini v1.8.4/go.mod h1:ZWy7j6v1vWGmPReu0iSGvRiise4YI5SkR3OHKTZ6Wuc= github.com/aws/aws-sdk-go-v2/internal/ini v1.8.5/go.mod h1:O3h0IK87yXci+kg6flUKzJnWeziQUKciKrLjcatSNcY=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18 h1:eZioDaZGJ0tMM4gzmkNIO2aAoQd+je7Ug7TkvAzlmkU= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20 h1:qi3e/dmpdONhj1RyIZdi6DKKpDXS5Lb8ftr3p7cyHJc=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.18/go.mod h1:CCXwUKAJdoWr6/NcxZ+zsiPr6oH/Q5aTooRGYieAyj4= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.20/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5 h1:CeY9LUdur+Dxoeldqoun6y4WtJ3RQtzk0JMP2gfUay0= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6 h1:XAq62tBTJP/85lFD5oqOOe7YYgWxY9LvWq8plyDvDVg=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.5/go.mod h1:AZLZf2fMaahW5s/wMRciu1sYbdsikT/UHwbUjOdEVTc= github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.6/go.mod h1:x0nZssQ3qZSnIcePWLvcoFisRXJzcTVvYpAAdYX8+GI=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10 h1:fJvQ5mIBVfKtiyx0AHY6HeWcRX5LGANLpq8SVR+Uazs= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11 h1:BYf7XNsJMzl4mObARUBUib+j2tf0U//JAAtTnYqvqCw=
github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.10/go.mod h1:Kzm5e6OmNH8VMkgK9t+ry5jEih4Y8whqs+1hrkxim1I= github.com/aws/aws-sdk-go-v2/service/internal/checksum v1.9.11/go.mod h1:aEUS4WrNk/+FxkBZZa7tVgp4pGH+kFGW40Y8rCPqt5g=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18 h1:LTRCYFlnnKFlKsyIQxKhJuDuA3ZkrDQMRYm6rXiHlLY= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19 h1:X1Tow7suZk9UCJHE1Iw9GMZJJl0dAnKXXP1NaSDHwmw=
github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.18/go.mod h1:XhwkgGG6bHSd00nO/mexWTcTjgd6PjuvWQMqSn2UaEk= github.com/aws/aws-sdk-go-v2/service/internal/presigned-url v1.13.19/go.mod h1:/rARO8psX+4sfjUQXp5LLifjUt8DuATZ31WptNJTyQA=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18 h1:/A/xDuZAVD2BpsS2fftFRo/NoEKQJ8YTnJDEHBy2Gtg= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19 h1:JnQeStZvPHFHeyky/7LbMlyQjUa+jIBj36OlWm0pzIk=
github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.18/go.mod h1:hWe9b4f+djUQGmyiGEeOnZv69dtMSgpDRIvNMvuvzvY= github.com/aws/aws-sdk-go-v2/service/internal/s3shared v1.19.19/go.mod h1:HGyasyHvYdFQeJhvDHfH7HXkHh57htcJGKDZ+7z+I24=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2 h1:M1A9AjcFwlxTLuf0Faj88L8Iqw0n/AJHjpZTQzMMsSc= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0 h1:zyKY4OxzUImu+DigelJI9o49QQv8CjREs5E1CywjtIA=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.2/go.mod h1:KsdTV6Q9WKUZm2mNJnUFmIoXfZux91M3sr/a4REX8e0= github.com/aws/aws-sdk-go-v2/service/s3 v1.97.0/go.mod h1:NF3JcMGOiARAss1ld3WGORCw71+4ExDD2cbbdKS5PpA=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6 h1:MzORe+J94I+hYu2a6XmV5yC9huoTv8NRcCrUNedDypQ= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7 h1:Y2cAXlClHsXkkOvWZFXATr34b0hxxloeQu/pAZz2row=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.6/go.mod h1:hXzcHLARD7GeWnifd8j9RWqtfIgxj4/cAtIVIK7hg8g= github.com/aws/aws-sdk-go-v2/service/signin v1.0.7/go.mod h1:idzZ7gmDeqeNrSPkdbtMp9qWMgcBwykA7P7Rzh5DXVU=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11 h1:7oGD8KPfBOJGXiCoRKrrrQkbvCp8N++u36hrLMPey6o= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12 h1:iSsvB9EtQ09YrsmIc44Heqlx5ByGErqhPK1ZQLppias=
github.com/aws/aws-sdk-go-v2/service/sso v1.30.11/go.mod h1:0DO9B5EUJQlIDif+XJRWCljZRKsAFKh3gpFz7UnDtOo= github.com/aws/aws-sdk-go-v2/service/sso v1.30.12/go.mod h1:fEWYKTRGoZNl8tZ77i61/ccwOMJdGxwOhWCkp6TXAr0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15 h1:edCcNp9eGIUDUCrzoCu1jWAXLGFIizeqkdkKgRlJwWc= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16 h1:EnUdUqRP1CNzt2DkV67tJx6XDN4xlfBFm+bzeNOQVb0=
github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.15/go.mod h1:lyRQKED9xWfgkYC/wmmYfv7iVIM68Z5OQ88ZdcV1QbU= github.com/aws/aws-sdk-go-v2/service/ssooidc v1.35.16/go.mod h1:Jic/xv0Rq/pFNCh3WwpH4BEqdbSAl+IyHro8LbibHD8=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7 h1:NITQpgo9A5NrDZ57uOWj+abvXSb83BbyggcUBVksN7c= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 h1:XQTQTF75vnug2TXS8m7CVJfC2nniYPZnO1D4Np761Oo=
github.com/aws/aws-sdk-go-v2/service/sts v1.41.7/go.mod h1:sks5UWBhEuWYDPdwlnRFn1w7xWdH29Jcpe+/PJQefEs= github.com/aws/aws-sdk-go-v2/service/sts v1.41.8/go.mod h1:Xgx+PR1NUOjNmQY+tRMnouRp83JRM8pRMw/vCaVhPkI=
github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc= github.com/aws/smithy-go v1.24.2/go.mod h1:YE2RhdIuDbA5E5bTdciG9KrW3+TiEONeUWCqxX9i1Fc=
github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA= github.com/benbjohnson/clock v1.1.0/go.mod h1:J11/hYXuz8f4ySSvYwY0FKfm+ezbsZBKZxNJlLklBHA=
@@ -112,8 +112,8 @@ github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U= github.com/go-test/deep v1.1.1 h1:0r/53hagsehfO4bzD2Pgr/+RgHqhmf+k1Bpse2cTu1U=
github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE= github.com/go-test/deep v1.1.1/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.6 h1:p8HrPJzOakx/mn/bQtjgNjdTcN+/S6FcG2CTtQOrHVU=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.6/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q=
github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q=
github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4=
@@ -296,20 +296,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s=
go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM=
go.opentelemetry.io/otel v1.40.0 h1:oA5YeOcpRTXq6NN7frwmwFR0Cn3RhTVZvXsP4duvCms= go.opentelemetry.io/otel v1.42.0 h1:lSQGzTgVR3+sgJDAU/7/ZMjN9Z+vUip7leaqBKy4sho=
go.opentelemetry.io/otel v1.40.0/go.mod h1:IMb+uXZUKkMXdPddhwAHm6UfOwJyh4ct1ybIlV14J0g= go.opentelemetry.io/otel v1.42.0/go.mod h1:lJNsdRMxCUIWuMlVJWzecSMuNjE7dOYyWlqOXWkdqCc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0/go.mod h1:bTdK1nhqF76qiPoCCdyFIV+N/sRHYXYCTQc+3VCi3MI=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/metric v1.40.0 h1:rcZe317KPftE2rstWIBitCdVp89A2HqjkxR3c11+p9g= go.opentelemetry.io/otel/metric v1.42.0 h1:2jXG+3oZLNXEPfNmnpxKDeZsFI5o4J+nz6xUlaFdF/4=
go.opentelemetry.io/otel/metric v1.40.0/go.mod h1:ib/crwQH7N3r5kfiBZQbwrTge743UDc7DTFVZrrXnqc= go.opentelemetry.io/otel/metric v1.42.0/go.mod h1:RlUN/7vTU7Ao/diDkEpQpnz3/92J9ko05BIwxYa2SSI=
go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.40.0 h1:WA4etStDttCSYuhwvEa8OP8I5EWu24lkOzp+ZYblVjw= go.opentelemetry.io/otel/trace v1.42.0 h1:OUCgIPt+mzOnaUTpOQcBiM/PLQ/Op7oq6g4LenLmOYY=
go.opentelemetry.io/otel/trace v1.40.0/go.mod h1:zeAhriXecNGP/s2SEG3+Y8X9ujcJOTqQ5RgdEJcawiA= go.opentelemetry.io/otel/trace v1.42.0/go.mod h1:f3K9S+IFqnumBkKhRJMeaZeNk9epyhnCmQh/EysQCdc=
go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A= go.opentelemetry.io/proto/otlp v1.9.0 h1:l706jCMITVouPOqEnii2fIAuO3IVGBRPV5ICjceRb/A=
go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4= go.opentelemetry.io/proto/otlp v1.9.0/go.mod h1:xE+Cx5E/eEHw+ISFkwPLwCZefwVjY+pqKg1qcK03+/4=
go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc= go.uber.org/atomic v1.7.0/go.mod h1:fEN4uk6kAWBTFdckzkM89CLk9XfWZrxpCo0nPH17wJc=
@@ -324,14 +324,14 @@ go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN8
go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI= go.uber.org/zap v1.19.1/go.mod h1:j3DNczoxDZroyBnOT1L/Q79cfUMGZxlv/9dzN7SM1rI=
go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= go.uber.org/zap v1.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= go.yaml.in/yaml/v2 v2.4.4 h1:tuyd0P+2Ont/d6e2rl3be67goVK4R6deVxCUX5vyPaQ=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.4/go.mod h1:gMZqIpDtDqOfM0uNfy0SkpRhvUryYH0Z6wdMYcacYXQ=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190930215403-16217165b5de/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
@@ -346,15 +346,15 @@ golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwY
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM= golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96bSt6lcn1PtDYWL6XObtHCRCNQM=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190911185100-cd5d95a43a6e/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -370,20 +370,20 @@ golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= golang.org/x/text v0.3.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ=
golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.15.0 h1:bbrp8t3bGUeFOx08pvsMYRTCVSMk89u4tKbNOZbp88U=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.15.0/go.mod h1:Y4YMaQmXwGQZoFaVFk4YpCt4FLQMYKZe9oeV/f4MSno=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190425150028-36563e24a262/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
@@ -399,12 +399,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4= google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c h1:OyQPd6I3pN/9gDxz6L13kYGJgqkpdrAohJRBeXyxlgI=
google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng= google.golang.org/genproto/googleapis/api v0.0.0-20260311181403-84a4fc48630c/go.mod h1:X2gu9Qwng7Nn009s/r3RUxqkzQNqOrAy79bluY7ojIg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c h1:xgCzyF2LFIO/0X2UAoVRiXKU5Xg6VjToG4i2/ecSswk=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8= google.golang.org/genproto/googleapis/rpc v0.0.0-20260311181403-84a4fc48630c/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= google.golang.org/grpc v1.79.2 h1:fRMD94s2tITpyJGtBBn7MkMseNpOZU8ZxgC3MMBaXRU=
google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/grpc v1.79.2/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View File

@@ -159,7 +159,7 @@ func (s *service) VerifyAccount(
token, err := s.vdb.Create( token, err := s.vdb.Create(
ctx, ctx,
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, ""). verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
WithTTL(time.Duration(time.Hour*24)), WithTTL(time.Hour * 24),
) )
if err != nil { if err != nil {
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct)) s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
@@ -238,7 +238,7 @@ func (s *service) ResetPassword(
return s.vdb.Create( return s.vdb.Create(
ctx, ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, ""). verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
WithTTL(time.Duration(time.Hour*1)), WithTTL(time.Hour),
) )
} }
@@ -250,7 +250,7 @@ func (s *service) UpdateLogin(
return s.vdb.Create( return s.vdb.Create(
ctx, ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin). verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
WithTTL(time.Duration(time.Hour*1)), WithTTL(time.Hour),
) )
} }

View File

@@ -164,6 +164,7 @@ func (e Endpoint) DecodeIBAN() (IBANEndpoint, error) {
func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) { func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint, error) {
if old == nil { if old == nil {
//nolint:nilnil // Nil legacy endpoint means no endpoint provided.
return nil, nil return nil, nil
} }
@@ -202,6 +203,7 @@ func LegacyPaymentEndpointToEndpointDTO(old *LegacyPaymentEndpoint) (*Endpoint,
func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) { func EndpointDTOToLegacyPaymentEndpoint(new *Endpoint) (*LegacyPaymentEndpoint, error) {
if new == nil { if new == nil {
//nolint:nilnil // Nil endpoint DTO means no endpoint provided.
return nil, nil return nil, nil
} }

View File

@@ -73,9 +73,10 @@ func validateQuoteIdempotency(previewOnly bool, idempotencyKey string) error {
} }
type InitiatePayment struct { type InitiatePayment struct {
PaymentBase `json:",inline"` PaymentBase `json:",inline"`
Intent *PaymentIntent `json:"intent,omitempty"` Intent *PaymentIntent `json:"intent,omitempty"`
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
ClientPaymentRef string `json:"clientPaymentRef,omitempty"`
} }
func (r InitiatePayment) Validate() error { func (r InitiatePayment) Validate() error {
@@ -106,8 +107,9 @@ func (r InitiatePayment) Validate() error {
} }
type InitiatePayments struct { type InitiatePayments struct {
PaymentBase `json:",inline"` PaymentBase `json:",inline"`
QuoteRef string `json:"quoteRef,omitempty"` QuoteRef string `json:"quoteRef,omitempty"`
ClientPaymentRef string `json:"clientPaymentRef,omitempty"`
} }
func (r *InitiatePayments) Validate() error { func (r *InitiatePayments) Validate() error {

View File

@@ -14,6 +14,7 @@ type PaymentIntent struct {
SettlementMode SettlementMode `json:"settlement_mode,omitempty"` SettlementMode SettlementMode `json:"settlement_mode,omitempty"`
FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"` FeeTreatment FeeTreatment `json:"fee_treatment,omitempty"`
Attributes map[string]string `json:"attributes,omitempty"` Attributes map[string]string `json:"attributes,omitempty"`
Comment string `json:"comment,omitempty"`
Customer *Customer `json:"customer,omitempty"` Customer *Customer `json:"customer,omitempty"`
} }

View File

@@ -0,0 +1,33 @@
package sresponse
import (
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model"
)
type callbackWriteResponse struct {
AccessToken TokenData `json:"accessToken"`
Callbacks []model.Callback `json:"callbacks"`
GeneratedSigningSecret string `json:"generatedSigningSecret,omitempty"`
}
func Callback(
logger mlogger.Logger,
callback *model.Callback,
accessToken *TokenData,
generatedSecret string,
created bool,
) http.HandlerFunc {
resp := callbackWriteResponse{
AccessToken: *accessToken,
Callbacks: []model.Callback{*callback},
GeneratedSigningSecret: generatedSecret,
}
if created {
return response.Created(logger, resp)
}
return response.Ok(logger, resp)
}

View File

@@ -8,12 +8,17 @@ import (
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mservice"
paymenttypes "github.com/tech/sendico/pkg/payments/types" paymenttypes "github.com/tech/sendico/pkg/payments/types"
feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1"
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1"
oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
"github.com/tech/sendico/server/interface/api/srequest"
"go.mongodb.org/mongo-driver/v2/bson"
"google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/timestamppb"
) )
@@ -65,26 +70,48 @@ type PaymentQuotes struct {
} }
type Payment struct { type Payment struct {
PaymentRef string `json:"paymentRef,omitempty"` PaymentRef string `json:"paymentRef,omitempty"`
IdempotencyKey string `json:"idempotencyKey,omitempty"` State string `json:"state,omitempty"`
State string `json:"state,omitempty"` Comment string `json:"comment,omitempty"`
FailureCode string `json:"failureCode,omitempty"` Source *PaymentEndpoint `json:"source"`
FailureReason string `json:"failureReason,omitempty"` Destination *PaymentEndpoint `json:"destination"`
Operations []PaymentOperation `json:"operations,omitempty"` FailureCode string `json:"failureCode,omitempty"`
LastQuote *PaymentQuote `json:"lastQuote,omitempty"` FailureReason string `json:"failureReason,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"` Operations []PaymentOperation `json:"operations,omitempty"`
Meta map[string]string `json:"meta,omitempty"` LastQuote *PaymentQuote `json:"lastQuote,omitempty"`
CreatedAt time.Time `json:"createdAt,omitempty"`
Meta map[string]string `json:"meta,omitempty"`
}
type PaymentEndpoint struct {
Type string `json:"type,omitempty"`
Data any `json:"data,omitempty"`
PaymentMethodRef string `json:"paymentMethodRef,omitempty"`
PayeeRef string `json:"payeeRef,omitempty"`
} }
type PaymentOperation struct { type PaymentOperation struct {
StepRef string `json:"stepRef,omitempty"` StepRef string `json:"stepRef,omitempty"`
Code string `json:"code,omitempty"` Code string `json:"code,omitempty"`
State string `json:"state,omitempty"` State string `json:"state,omitempty"`
Label string `json:"label,omitempty"` Label string `json:"label,omitempty"`
FailureCode string `json:"failureCode,omitempty"` Money *PaymentOperationMoney `json:"money,omitempty"`
FailureReason string `json:"failureReason,omitempty"` OperationRef string `json:"operationRef,omitempty"`
StartedAt time.Time `json:"startedAt,omitempty"` Gateway string `json:"gateway,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"` FailureCode string `json:"failureCode,omitempty"`
FailureReason string `json:"failureReason,omitempty"`
StartedAt time.Time `json:"startedAt,omitempty"`
CompletedAt time.Time `json:"completedAt,omitempty"`
}
type PaymentOperationMoney struct {
Planned *PaymentOperationMoneySnapshot `json:"planned,omitempty"`
Executed *PaymentOperationMoneySnapshot `json:"executed,omitempty"`
}
type PaymentOperationMoneySnapshot struct {
Amount *paymenttypes.Money `json:"amount,omitempty"`
ConvertedAmount *paymenttypes.Money `json:"convertedAmount,omitempty"`
} }
type paymentQuoteResponse struct { type paymentQuoteResponse struct {
@@ -283,21 +310,257 @@ func toPayment(p *orchestrationv2.Payment) *Payment {
if p == nil { if p == nil {
return nil return nil
} }
intent := p.GetIntentSnapshot()
operations := toUserVisibleOperations(p.GetStepExecutions()) operations := toUserVisibleOperations(p.GetStepExecutions())
failureCode, failureReason := firstFailure(operations) failureCode, failureReason := firstFailure(operations)
return &Payment{ return &Payment{
PaymentRef: p.GetPaymentRef(), PaymentRef: p.GetPaymentRef(),
State: enumJSONName(p.GetState().String()), State: enumJSONName(p.GetState().String()),
FailureCode: failureCode, Comment: strings.TrimSpace(intent.GetComment()),
FailureReason: failureReason, Source: toPaymentEndpoint(intent.GetSource()),
Operations: operations, Destination: toPaymentEndpoint(intent.GetDestination()),
LastQuote: toPaymentQuote(p.GetQuoteSnapshot()), FailureCode: failureCode,
CreatedAt: timestampAsTime(p.GetCreatedAt()), FailureReason: failureReason,
Meta: paymentMeta(p), Operations: operations,
IdempotencyKey: "", LastQuote: toPaymentQuote(p.GetQuoteSnapshot()),
CreatedAt: timestampAsTime(p.GetCreatedAt()),
Meta: paymentMeta(p),
} }
} }
func toPaymentEndpoint(endpoint *endpointv1.PaymentEndpoint) *PaymentEndpoint {
if endpoint == nil {
return nil
}
if paymentMethodRef := strings.TrimSpace(endpoint.GetPaymentMethodRef()); paymentMethodRef != "" {
return &PaymentEndpoint{PaymentMethodRef: paymentMethodRef}
}
if payeeRef := strings.TrimSpace(endpoint.GetPayeeRef()); payeeRef != "" {
return &PaymentEndpoint{PayeeRef: payeeRef}
}
method := endpoint.GetPaymentMethod()
if method == nil {
return nil
}
return &PaymentEndpoint{
Type: paymentEndpointType(method.GetType()),
Data: paymentEndpointData(method),
}
}
func paymentEndpointType(methodType endpointv1.PaymentMethodType) string {
switch methodType {
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
return string(srequest.EndpointTypeIBAN)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
return string(srequest.EndpointTypeCard)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
return string(srequest.EndpointTypeCardToken)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
return string(srequest.EndpointTypeBankAccount)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
return string(srequest.EndpointTypeWallet)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
return string(srequest.EndpointTypeExternalChain)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
return string(srequest.EndpointTypeLedger)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_ACCOUNT:
return "account"
default:
return "unspecified"
}
}
func paymentEndpointData(method *endpointv1.PaymentMethod) any {
if method == nil {
return nil
}
switch method.GetType() {
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER:
type ledgerMethodData struct {
LedgerAccountRef string `bson:"ledgerAccountRef"`
ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty"`
}
var payload ledgerMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
return srequest.LedgerEndpoint{
LedgerAccountRef: strings.TrimSpace(payload.LedgerAccountRef),
ContraLedgerAccountRef: strings.TrimSpace(payload.ContraLedgerAccountRef),
}
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET:
type walletMethodData struct {
WalletID string `bson:"walletId"`
}
var payload walletMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
return srequest.WalletEndpoint{
WalletID: strings.TrimSpace(payload.WalletID),
}
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS:
type cryptoMethodData struct {
Currency string `bson:"currency"`
Address string `bson:"address"`
Network string `bson:"network"`
DestinationTag *string `bson:"destinationTag,omitempty"`
}
var payload cryptoMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
endpoint := srequest.ExternalChainEndpoint{
Asset: &srequest.Asset{
Chain: parseChainNetwork(payload.Network),
TokenSymbol: strings.ToUpper(strings.TrimSpace(payload.Currency)),
},
Address: strings.TrimSpace(payload.Address),
}
if memo := strings.TrimSpace(strPtr(payload.DestinationTag)); memo != "" {
endpoint.Memo = memo
}
if endpoint.Asset.Chain == srequest.ChainNetworkUnspecified && endpoint.Asset.TokenSymbol == "" {
endpoint.Asset = nil
}
return endpoint
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
type cardMethodData struct {
Pan string `bson:"pan"`
FirstName string `bson:"firstName"`
LastName string `bson:"lastName"`
ExpMonth string `bson:"expMonth"`
ExpYear string `bson:"expYear"`
Country string `bson:"country,omitempty"`
}
var payload cardMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
return srequest.CardEndpoint{
Pan: strings.TrimSpace(payload.Pan),
FirstName: strings.TrimSpace(payload.FirstName),
LastName: strings.TrimSpace(payload.LastName),
ExpMonth: parseUint32(payload.ExpMonth),
ExpYear: parseUint32(payload.ExpYear),
Country: strings.TrimSpace(payload.Country),
}
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN:
type cardTokenMethodData struct {
Token string `bson:"token"`
Last4 string `bson:"last4,omitempty"`
}
var payload cardTokenMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
return srequest.CardTokenEndpoint{
Token: strings.TrimSpace(payload.Token),
MaskedPan: strings.TrimSpace(payload.Last4),
}
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
type bankAccountMethodData struct {
RecipientName string `bson:"recipientName"`
Inn string `bson:"inn"`
Kpp string `bson:"kpp"`
BankName string `bson:"bankName"`
Bik string `bson:"bik"`
AccountNumber string `bson:"accountNumber"`
CorrespondentAccount string `bson:"correspondentAccount"`
}
var payload bankAccountMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
return srequest.BankAccountEndpoint{
RecipientName: strings.TrimSpace(payload.RecipientName),
Inn: strings.TrimSpace(payload.Inn),
Kpp: strings.TrimSpace(payload.Kpp),
BankName: strings.TrimSpace(payload.BankName),
Bik: strings.TrimSpace(payload.Bik),
AccountNumber: strings.TrimSpace(payload.AccountNumber),
CorrespondentAccount: strings.TrimSpace(payload.CorrespondentAccount),
}
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
type ibanMethodData struct {
IBAN string `bson:"iban"`
AccountHolder string `bson:"accountHolder"`
BIC *string `bson:"bic,omitempty"`
BankName *string `bson:"bankName,omitempty"`
}
var payload ibanMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return toRawBSON(method.GetData())
}
return srequest.IBANEndpoint{
IBAN: strings.TrimSpace(payload.IBAN),
AccountHolder: strings.TrimSpace(payload.AccountHolder),
BIC: strings.TrimSpace(strPtr(payload.BIC)),
BankName: strings.TrimSpace(strPtr(payload.BankName)),
}
default:
return toRawBSON(method.GetData())
}
}
func toRawBSON(raw []byte) map[string]any {
if len(raw) == 0 {
return nil
}
var data map[string]any
if err := bson.Unmarshal(raw, &data); err != nil {
return nil
}
if len(data) == 0 {
return nil
}
return data
}
func parseChainNetwork(value string) srequest.ChainNetwork {
switch strings.ToUpper(strings.TrimSpace(value)) {
case "ETHEREUM_MAINNET":
return srequest.ChainNetworkEthereumMainnet
case "ARBITRUM_ONE":
return srequest.ChainNetworkArbitrumOne
case "TRON_MAINNET":
return srequest.ChainNetworkTronMainnet
case "TRON_NILE":
return srequest.ChainNetworkTronNile
case "", "UNSPECIFIED":
return srequest.ChainNetworkUnspecified
default:
return srequest.ChainNetwork(strings.ToLower(strings.TrimSpace(value)))
}
}
func parseUint32(value string) uint32 {
clean := strings.TrimSpace(value)
if clean == "" {
return 0
}
parsed, err := strconv.ParseUint(clean, 10, 32)
if err != nil {
return 0
}
return uint32(parsed)
}
func strPtr(v *string) string {
if v == nil {
return ""
}
return *v
}
func firstFailure(operations []PaymentOperation) (string, string) { func firstFailure(operations []PaymentOperation) (string, string) {
for _, op := range operations { for _, op := range operations {
if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" { if strings.TrimSpace(op.FailureCode) == "" && strings.TrimSpace(op.FailureReason) == "" {
@@ -326,13 +589,21 @@ func toUserVisibleOperations(steps []*orchestrationv2.StepExecution) []PaymentOp
} }
func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation { func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
operationRef, gateway := operationRefAndGateway(step.GetStepCode(), step.GetRefs())
plannedAmount := stepPlannedAmount(step)
plannedConvertedAmount := stepPlannedConvertedAmount(step)
executedAmount := stepExecutedAmount(step)
executedConvertedAmount := stepExecutedConvertedAmount(step)
op := PaymentOperation{ op := PaymentOperation{
StepRef: step.GetStepRef(), StepRef: step.GetStepRef(),
Code: step.GetStepCode(), Code: step.GetStepCode(),
State: enumJSONName(step.GetState().String()), State: enumJSONName(step.GetState().String()),
Label: strings.TrimSpace(step.GetUserLabel()), Label: strings.TrimSpace(step.GetUserLabel()),
StartedAt: timestampAsTime(step.GetStartedAt()), Money: toOperationMoney(plannedAmount, plannedConvertedAmount, executedAmount, executedConvertedAmount),
CompletedAt: timestampAsTime(step.GetCompletedAt()), OperationRef: operationRef,
Gateway: gateway,
StartedAt: timestampAsTime(step.GetStartedAt()),
CompletedAt: timestampAsTime(step.GetCompletedAt()),
} }
failure := step.GetFailure() failure := step.GetFailure()
if failure == nil { if failure == nil {
@@ -346,6 +617,215 @@ func toPaymentOperation(step *orchestrationv2.StepExecution) PaymentOperation {
return op return op
} }
func stepPlannedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
if step == nil {
return nil
}
if money := step.GetMoney(); money != nil {
if planned := money.GetPlanned(); planned != nil {
if normalized := normalizeOperationMoney(toMoney(planned.GetAmount())); normalized != nil {
return normalized
}
}
}
return nil
}
func stepPlannedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
if step == nil {
return nil
}
if money := step.GetMoney(); money != nil {
if planned := money.GetPlanned(); planned != nil {
if normalized := normalizeOperationMoney(toMoney(planned.GetConvertedAmount())); normalized != nil {
return normalized
}
}
}
return nil
}
func stepExecutedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
if step == nil {
return nil
}
if money := step.GetMoney(); money != nil {
if executed := money.GetExecuted(); executed != nil {
if normalized := normalizeOperationMoney(toMoney(executed.GetAmount())); normalized != nil {
return normalized
}
}
}
return nil
}
func stepExecutedConvertedAmount(step *orchestrationv2.StepExecution) *paymenttypes.Money {
if step == nil {
return nil
}
if money := step.GetMoney(); money != nil {
if executed := money.GetExecuted(); executed != nil {
if normalized := normalizeOperationMoney(toMoney(executed.GetConvertedAmount())); normalized != nil {
return normalized
}
}
}
return nil
}
func toOperationMoney(
plannedAmount *paymenttypes.Money,
plannedConvertedAmount *paymenttypes.Money,
executedAmount *paymenttypes.Money,
executedConvertedAmount *paymenttypes.Money,
) *PaymentOperationMoney {
planned := toOperationMoneySnapshot(plannedAmount, plannedConvertedAmount)
executed := toOperationMoneySnapshot(executedAmount, executedConvertedAmount)
if planned == nil && executed == nil {
return nil
}
return &PaymentOperationMoney{
Planned: planned,
Executed: executed,
}
}
func toOperationMoneySnapshot(amount *paymenttypes.Money, convertedAmount *paymenttypes.Money) *PaymentOperationMoneySnapshot {
if amount == nil && convertedAmount == nil {
return nil
}
return &PaymentOperationMoneySnapshot{
Amount: amount,
ConvertedAmount: convertedAmount,
}
}
func normalizeOperationMoney(value *paymenttypes.Money) *paymenttypes.Money {
if value == nil {
return nil
}
amount := strings.TrimSpace(value.GetAmount())
currency := strings.TrimSpace(value.GetCurrency())
if amount == "" || currency == "" {
return nil
}
return &paymenttypes.Money{
Amount: amount,
Currency: currency,
}
}
const (
externalRefKindOperation = "operation_ref"
)
func operationRefAndGateway(stepCode string, refs []*orchestrationv2.ExternalReference) (string, mservice.Type) {
var (
operationRef string
gateway mservice.Type
)
for _, ref := range refs {
if ref == nil {
continue
}
kind := strings.ToLower(strings.TrimSpace(ref.GetKind()))
value := strings.TrimSpace(ref.GetRef())
candidateGateway := inferGatewayType(ref.GetGatewayInstanceId(), ref.GetRail(), stepCode)
if kind == externalRefKindOperation && operationRef == "" && value != "" {
operationRef = value
}
if gateway == "" && candidateGateway != "" {
gateway = candidateGateway
}
}
if gateway == "" {
gateway = inferGatewayType("", gatewayv1.Rail_RAIL_UNSPECIFIED, stepCode)
}
return operationRef, gateway
}
func inferGatewayType(gatewayInstanceID string, rail gatewayv1.Rail, stepCode string) mservice.Type {
if gateway := gatewayTypeFromInstanceID(gatewayInstanceID); gateway != "" {
return gateway
}
if gateway := gatewayTypeFromRail(rail); gateway != "" {
return gateway
}
return gatewayTypeFromStepCode(stepCode)
}
func gatewayTypeFromInstanceID(raw string) mservice.Type {
value := strings.ToLower(strings.TrimSpace(raw))
if value == "" {
return ""
}
switch value {
case mservice.ChainGateway, mservice.TronGateway, mservice.MntxGateway, mservice.PaymentGateway, mservice.TgSettle, mservice.Ledger:
return value
}
switch {
case strings.Contains(value, "ledger"):
return mservice.Ledger
case strings.Contains(value, "tgsettle"):
return mservice.TgSettle
case strings.Contains(value, "payment_gateway"),
strings.Contains(value, "settlement"),
strings.Contains(value, "onramp"),
strings.Contains(value, "offramp"):
return mservice.PaymentGateway
case strings.Contains(value, "mntx"), strings.Contains(value, "mcards"):
return mservice.MntxGateway
case strings.Contains(value, "tron"):
return mservice.TronGateway
case strings.Contains(value, "chain"), strings.Contains(value, "crypto"):
return mservice.ChainGateway
case strings.Contains(value, "card"):
return mservice.MntxGateway
default:
return ""
}
}
func gatewayTypeFromRail(rail gatewayv1.Rail) mservice.Type {
switch rail {
case gatewayv1.Rail_RAIL_LEDGER:
return mservice.Ledger
case gatewayv1.Rail_RAIL_CARD:
return mservice.MntxGateway
case gatewayv1.Rail_RAIL_SETTLEMENT, gatewayv1.Rail_RAIL_ONRAMP, gatewayv1.Rail_RAIL_OFFRAMP:
return mservice.PaymentGateway
case gatewayv1.Rail_RAIL_CRYPTO:
return mservice.ChainGateway
default:
return ""
}
}
func gatewayTypeFromStepCode(stepCode string) mservice.Type {
code := strings.ToLower(strings.TrimSpace(stepCode))
switch {
case strings.Contains(code, "ledger"):
return mservice.Ledger
case strings.Contains(code, "card_payout"), strings.Contains(code, ".card."):
return mservice.MntxGateway
case strings.Contains(code, "provider_settlement"),
strings.Contains(code, "settlement"),
strings.Contains(code, "fx_convert"),
strings.Contains(code, "onramp"),
strings.Contains(code, "offramp"):
return mservice.PaymentGateway
case strings.Contains(code, "crypto"), strings.Contains(code, "chain"):
return mservice.ChainGateway
default:
return ""
}
}
func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool { func isUserVisibleStep(visibility orchestrationv2.ReportVisibility) bool {
switch visibility { switch visibility {
case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN, case orchestrationv2.ReportVisibility_REPORT_VISIBILITY_HIDDEN,

View File

@@ -3,9 +3,14 @@ package sresponse
import ( import (
"testing" "testing"
gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
"github.com/tech/sendico/server/interface/api/srequest"
"go.mongodb.org/mongo-driver/v2/bson"
) )
func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) { func TestToUserVisibleOperationsFiltersByVisibility(t *testing.T) {
@@ -119,6 +124,141 @@ func TestToPaymentIgnoresHiddenFailures(t *testing.T) {
} }
} }
func TestToPaymentMapsIntentComment(t *testing.T) {
dto := toPayment(&orchestrationv2.Payment{
PaymentRef: "pay-3",
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
IntentSnapshot: &quotationv2.QuoteIntent{
Comment: " invoice-7 ",
},
})
if dto == nil {
t.Fatal("expected non-nil payment dto")
}
if got, want := dto.Comment, "invoice-7"; got != want {
t.Fatalf("comment mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentMapsSourceAndDestination(t *testing.T) {
sourceRaw, err := bson.Marshal(struct {
WalletID string `bson:"walletId"`
}{
WalletID: "wallet-src-1",
})
if err != nil {
t.Fatalf("marshal source method data: %v", err)
}
destinationRaw, err := bson.Marshal(struct {
Currency string `bson:"currency"`
Address string `bson:"address"`
Network string `bson:"network"`
DestinationTag *string `bson:"destinationTag,omitempty"`
}{
Currency: "USDT",
Address: "TXabc",
Network: "TRON_MAINNET",
})
if err != nil {
t.Fatalf("marshal destination method data: %v", err)
}
dto := toPayment(&orchestrationv2.Payment{
PaymentRef: "pay-src-dst",
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
IntentSnapshot: &quotationv2.QuoteIntent{
Source: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
PaymentMethod: &endpointv1.PaymentMethod{
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET,
Data: sourceRaw,
},
},
},
Destination: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
PaymentMethod: &endpointv1.PaymentMethod{
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS,
Data: destinationRaw,
},
},
},
},
})
if dto == nil {
t.Fatal("expected non-nil payment dto")
}
if dto.Source == nil {
t.Fatal("expected source endpoint")
}
if got, want := dto.Source.Type, string(srequest.EndpointTypeWallet); got != want {
t.Fatalf("source type mismatch: got=%q want=%q", got, want)
}
sourceEndpoint, ok := dto.Source.Data.(srequest.WalletEndpoint)
if !ok {
t.Fatalf("source endpoint payload type mismatch: got=%T", dto.Source.Data)
}
if got, want := sourceEndpoint.WalletID, "wallet-src-1"; got != want {
t.Fatalf("source wallet id mismatch: got=%q want=%q", got, want)
}
if dto.Destination == nil {
t.Fatal("expected destination endpoint")
}
if got, want := dto.Destination.Type, string(srequest.EndpointTypeExternalChain); got != want {
t.Fatalf("destination type mismatch: got=%q want=%q", got, want)
}
destinationEndpoint, ok := dto.Destination.Data.(srequest.ExternalChainEndpoint)
if !ok {
t.Fatalf("destination endpoint payload type mismatch: got=%T", dto.Destination.Data)
}
if got, want := destinationEndpoint.Address, "TXabc"; got != want {
t.Fatalf("destination address mismatch: got=%q want=%q", got, want)
}
if destinationEndpoint.Asset == nil {
t.Fatal("expected destination asset")
}
if got, want := destinationEndpoint.Asset.TokenSymbol, "USDT"; got != want {
t.Fatalf("destination token mismatch: got=%q want=%q", got, want)
}
if got, want := destinationEndpoint.Asset.Chain, srequest.ChainNetworkTronMainnet; got != want {
t.Fatalf("destination chain mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentMapsEndpointRefs(t *testing.T) {
dto := toPayment(&orchestrationv2.Payment{
PaymentRef: "pay-refs",
State: orchestrationv2.OrchestrationState_ORCHESTRATION_STATE_CREATED,
IntentSnapshot: &quotationv2.QuoteIntent{
Source: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethodRef{
PaymentMethodRef: "pm-123",
},
},
Destination: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PayeeRef{
PayeeRef: "payee-777",
},
},
},
})
if dto == nil {
t.Fatal("expected non-nil payment dto")
}
if dto.Source == nil {
t.Fatal("expected source endpoint")
}
if got, want := dto.Source.PaymentMethodRef, "pm-123"; got != want {
t.Fatalf("source payment_method_ref mismatch: got=%q want=%q", got, want)
}
if dto.Destination == nil {
t.Fatal("expected destination endpoint")
}
if got, want := dto.Destination.PayeeRef, "payee-777"; got != want {
t.Fatalf("destination payee_ref mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentQuote_MapsIntentRef(t *testing.T) { func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
dto := toPaymentQuote(&quotationv2.PaymentQuote{ dto := toPaymentQuote(&quotationv2.PaymentQuote{
QuoteRef: "quote-1", QuoteRef: "quote-1",
@@ -134,3 +274,304 @@ func TestToPaymentQuote_MapsIntentRef(t *testing.T) {
t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want) t.Fatalf("intent_ref mismatch: got=%q want=%q", got, want)
} }
} }
func TestToPaymentOperation_MapsOperationRefAndGateway(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-1",
StepCode: "hop.4.card_payout.send",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
Refs: []*orchestrationv2.ExternalReference{
{
Rail: gatewayv1.Rail_RAIL_CARD,
GatewayInstanceId: "mcards",
Kind: "operation_ref",
Ref: "op-123",
},
},
})
if got, want := op.OperationRef, "op-123"; got != want {
t.Fatalf("operation_ref mismatch: got=%q want=%q", got, want)
}
if got, want := op.Gateway, "mntx_gateway"; got != want {
t.Fatalf("gateway mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentOperation_InfersGatewayFromStepCode(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-2",
StepCode: "edge.1_2.ledger.debit",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
})
if got := op.OperationRef; got != "" {
t.Fatalf("expected empty operation_ref, got=%q", got)
}
if got, want := op.Gateway, "ledger"; got != want {
t.Fatalf("gateway mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentOperation_DoesNotFallbackToCardPayoutRef(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-3",
StepCode: "hop.4.card_payout.send",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
Refs: []*orchestrationv2.ExternalReference{
{
Rail: gatewayv1.Rail_RAIL_CARD,
GatewayInstanceId: "mcards",
Kind: "card_payout_ref",
Ref: "payout-123",
},
},
})
if got := op.OperationRef; got != "" {
t.Fatalf("expected empty operation_ref, got=%q", got)
}
if got, want := op.Gateway, "mntx_gateway"; got != want {
t.Fatalf("gateway mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentOperation_MapsAmount(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-4",
StepCode: "hop.4.card_payout.send",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
})
if got := op.Money; got != nil {
t.Fatalf("expected nil money payload without step money, got=%+v", got)
}
}
func TestToPaymentOperation_PrefersExecutedMoney(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-4b",
StepCode: "hop.4.card_payout.send",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
Money: &orchestrationv2.StepExecutionMoney{
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "88.00", Currency: "EUR"},
},
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "99.95", Currency: "EUR"},
},
},
})
if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil {
t.Fatal("expected executed amount to be mapped")
}
if got, want := op.Money.Executed.Amount.Amount, "99.95"; got != want {
t.Fatalf("executed amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Executed.Amount.Currency, "EUR"; got != want {
t.Fatalf("executed amount.currency mismatch: got=%q want=%q", got, want)
}
if op.Money.Planned == nil || op.Money.Planned.Amount == nil {
t.Fatal("expected planned amount to be exposed")
}
if got, want := op.Money.Planned.Amount.Amount, "88.00"; got != want {
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Planned.Amount.Currency, "EUR"; got != want {
t.Fatalf("planned amount.currency mismatch: got=%q want=%q", got, want)
}
if got := op.Money.Executed.ConvertedAmount; got != nil {
t.Fatalf("expected no executed converted_amount for non-fx operation, got=%+v", got)
}
}
func TestToPaymentOperation_UsesPlannedMoneyBeforeExecution(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-4c",
StepCode: "hop.4.card_payout.send",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING,
Money: &orchestrationv2.StepExecutionMoney{
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "77.10", Currency: "USD"},
},
},
})
if op.Money == nil || op.Money.Planned == nil || op.Money.Planned.Amount == nil {
t.Fatal("expected planned amount from structured planned money")
}
if got, want := op.Money.Planned.Amount.Amount, "77.10"; got != want {
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Planned.Amount.Currency, "USD"; got != want {
t.Fatalf("planned amount.currency mismatch: got=%q want=%q", got, want)
}
if got := op.Money.Executed; got != nil {
t.Fatalf("expected no executed snapshot before execution, got=%+v", got)
}
}
func TestToPaymentOperation_UsesStructuredMoneyEnvelope(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-4d",
StepCode: "hop.4.card_payout.send",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING,
Money: &orchestrationv2.StepExecutionMoney{
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "66.00", Currency: "USD"},
},
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "67.00", Currency: "USD"},
},
},
})
if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil {
t.Fatal("expected amount from structured executed money")
}
if got, want := op.Money.Executed.Amount.Amount, "67.00"; got != want {
t.Fatalf("executed amount.value mismatch: got=%q want=%q", got, want)
}
if op.Money.Planned == nil || op.Money.Planned.Amount == nil {
t.Fatal("expected planned amount from structured money")
}
if got, want := op.Money.Planned.Amount.Amount, "66.00"; got != want {
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentOperation_MapsFxTwoAmounts(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-5",
StepCode: "hop.2.settlement.fx_convert",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
Money: &orchestrationv2.StepExecutionMoney{
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
},
},
})
if op.Money == nil || op.Money.Executed == nil {
t.Fatal("expected executed snapshot to be mapped")
}
if got := op.Money.Executed.Amount; got != nil {
t.Fatalf("expected nil base amount without executed_money, got=%+v", got)
}
if op.Money.Executed.ConvertedAmount == nil {
t.Fatal("expected fx converted amount to be mapped")
}
if got, want := op.Money.Executed.ConvertedAmount.Amount, "100.00"; got != want {
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Executed.ConvertedAmount.Currency, "EUR"; got != want {
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentOperation_UsesPlannedFxAmountsBeforeExecution(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-5b",
StepCode: "hop.2.settlement.fx_convert",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_PENDING,
Money: &orchestrationv2.StepExecutionMoney{
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
},
},
})
if op.Money == nil || op.Money.Planned == nil {
t.Fatal("expected planned snapshot from structured money")
}
if got, want := op.Money.Planned.Amount.Amount, "109.50"; got != want {
t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Planned.Amount.Currency, "USDT"; got != want {
t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want)
}
if op.Money.Planned.ConvertedAmount == nil {
t.Fatal("expected fx converted amount from structured planned money")
}
if got, want := op.Money.Planned.ConvertedAmount.Amount, "100.00"; got != want {
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Planned.ConvertedAmount.Currency, "EUR"; got != want {
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
}
if got := op.Money.Executed; got != nil {
t.Fatalf("expected nil executed snapshot before execution, got=%+v", got)
}
}
func TestToPaymentOperation_UsesStructuredFxMoneyEnvelope(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-5c",
StepCode: "hop.2.settlement.fx_convert",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
Money: &orchestrationv2.StepExecutionMoney{
Planned: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
},
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "110.00", Currency: "USDT"},
ConvertedAmount: &moneyv1.Money{Amount: "101.00", Currency: "EUR"},
},
},
})
if op.Money == nil || op.Money.Executed == nil || op.Money.Planned == nil {
t.Fatal("expected snapshots from structured money")
}
if got, want := op.Money.Executed.Amount.Amount, "110.00"; got != want {
t.Fatalf("amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Executed.ConvertedAmount.Amount, "101.00"; got != want {
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
}
if op.Money.Planned.Amount == nil || op.Money.Planned.ConvertedAmount == nil {
t.Fatal("expected planned amounts from structured money")
}
if got, want := op.Money.Planned.Amount.Amount, "109.50"; got != want {
t.Fatalf("planned amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Planned.ConvertedAmount.Amount, "100.00"; got != want {
t.Fatalf("planned converted amount.value mismatch: got=%q want=%q", got, want)
}
}
func TestToPaymentOperation_FxWithExecutedMoney_StillProvidesTwoAmounts(t *testing.T) {
op := toPaymentOperation(&orchestrationv2.StepExecution{
StepRef: "step-6",
StepCode: "hop.2.settlement.fx_convert",
State: orchestrationv2.StepExecutionState_STEP_EXECUTION_STATE_COMPLETED,
Money: &orchestrationv2.StepExecutionMoney{
Executed: &orchestrationv2.StepExecutionMoneySnapshot{
Amount: &moneyv1.Money{Amount: "109.50", Currency: "USDT"},
ConvertedAmount: &moneyv1.Money{Amount: "100.00", Currency: "EUR"},
},
},
})
if op.Money == nil || op.Money.Executed == nil || op.Money.Executed.Amount == nil {
t.Fatal("expected fx base amount to be mapped")
}
if got, want := op.Money.Executed.Amount.Amount, "109.50"; got != want {
t.Fatalf("base amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Executed.Amount.Currency, "USDT"; got != want {
t.Fatalf("base amount.currency mismatch: got=%q want=%q", got, want)
}
if op.Money.Executed.ConvertedAmount == nil {
t.Fatal("expected fx quote amount to be mapped")
}
if got, want := op.Money.Executed.ConvertedAmount.Amount, "100.00"; got != want {
t.Fatalf("converted amount.value mismatch: got=%q want=%q", got, want)
}
if got, want := op.Money.Executed.ConvertedAmount.Currency, "EUR"; got != want {
t.Fatalf("converted amount.currency mismatch: got=%q want=%q", got, want)
}
}

View File

@@ -110,7 +110,7 @@ func Account2ClaimsForClient(a *model.Account, expiration int, clientID string)
paramNameName: t.Name, paramNameName: t.Name,
paramNameLocale: t.Locale, paramNameLocale: t.Locale,
paramNameClientID: t.ClientID, paramNameClientID: t.ClientID,
paramNameExpiration: int64(t.Expiration.Unix()), paramNameExpiration: t.Expiration.Unix(),
paramNamePending: t.Pending, paramNamePending: t.Pending,
} }
} }

View File

@@ -26,11 +26,11 @@ const (
var ( var (
ledgerDiscoveryServiceNames = []string{ ledgerDiscoveryServiceNames = []string{
"LEDGER", "LEDGER",
string(mservice.Ledger), mservice.Ledger,
} }
paymentOrchestratorDiscoveryServiceNames = []string{ paymentOrchestratorDiscoveryServiceNames = []string{
"PAYMENTS_ORCHESTRATOR", "PAYMENTS_ORCHESTRATOR",
string(mservice.PaymentOrchestrator), mservice.PaymentOrchestrator,
} }
paymentQuotationDiscoveryServiceNames = []string{ paymentQuotationDiscoveryServiceNames = []string{
"PAYMENTS_QUOTATION", "PAYMENTS_QUOTATION",
@@ -41,7 +41,7 @@ var (
paymentMethodsDiscoveryServiceNames = []string{ paymentMethodsDiscoveryServiceNames = []string{
"PAYMENTS_METHODS", "PAYMENTS_METHODS",
"PAYMENT_METHODS", "PAYMENT_METHODS",
string(mservice.PaymentMethods), mservice.PaymentMethods,
} }
) )
@@ -339,13 +339,13 @@ func selectGatewayEndpoint(gateways []discovery.GatewaySummary, preferredNetwork
func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) { func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
raw = strings.TrimSpace(raw) raw = strings.TrimSpace(raw)
if raw == "" { if raw == "" {
return discoveryEndpoint{}, fmt.Errorf("Invoke uri is empty") return discoveryEndpoint{}, fmt.Errorf("invoke uri is empty")
} }
// Without a scheme we expect a plain host:port target. // Without a scheme we expect a plain host:port target.
if !strings.Contains(raw, "://") { if !strings.Contains(raw, "://") {
if _, _, err := net.SplitHostPort(raw); err != nil { if _, _, err := net.SplitHostPort(raw); err != nil {
return discoveryEndpoint{}, fmt.Errorf("Invoke uri must include host:port: %w", err) return discoveryEndpoint{}, fmt.Errorf("invoke uri must include host:port: %w", err)
} }
return discoveryEndpoint{ return discoveryEndpoint{
address: raw, address: raw,
@@ -363,7 +363,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
case "grpc": case "grpc":
address := strings.TrimSpace(parsed.Host) address := strings.TrimSpace(parsed.Host)
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil { if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
return discoveryEndpoint{}, fmt.Errorf("Grpc invoke uri must include host:port: %w", splitErr) return discoveryEndpoint{}, fmt.Errorf("grpc invoke uri must include host:port: %w", splitErr)
} }
return discoveryEndpoint{ return discoveryEndpoint{
address: address, address: address,
@@ -373,7 +373,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
case "grpcs": case "grpcs":
address := strings.TrimSpace(parsed.Host) address := strings.TrimSpace(parsed.Host)
if _, _, splitErr := net.SplitHostPort(address); splitErr != nil { if _, _, splitErr := net.SplitHostPort(address); splitErr != nil {
return discoveryEndpoint{}, fmt.Errorf("Grpcs invoke uri must include host:port: %w", splitErr) return discoveryEndpoint{}, fmt.Errorf("grpcs invoke uri must include host:port: %w", splitErr)
} }
return discoveryEndpoint{ return discoveryEndpoint{
address: address, address: address,
@@ -388,7 +388,7 @@ func parseDiscoveryInvokeURI(raw string) (discoveryEndpoint, error) {
raw: raw, raw: raw,
}, nil }, nil
default: default:
return discoveryEndpoint{}, fmt.Errorf("Unsupported invoke uri scheme: %s", parsed.Scheme) return discoveryEndpoint{}, fmt.Errorf("unsupported invoke uri scheme: %s", parsed.Scheme)
} }
} }

View File

@@ -42,9 +42,7 @@ func PrepareRefreshToken(
} }
token := &model.RefreshToken{ token := &model.RefreshToken{
AccountBoundBase: model.AccountBoundBase{ AccountRef: account.GetID(),
AccountRef: account.GetID(),
},
ClientRefreshToken: model.ClientRefreshToken{ ClientRefreshToken: model.ClientRefreshToken{
SessionIdentifier: *session, SessionIdentifier: *session,
RefreshToken: refreshToken, RefreshToken: refreshToken,

View File

@@ -43,6 +43,7 @@ func (d *DispatcherImpl) dispatchMessage(ctx context.Context, conn *websocket.Co
} }
func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) { func (d *DispatcherImpl) handle(w http.ResponseWriter, r *http.Request) {
//nolint:contextcheck // websocket.Handler callback signature does not carry request context.
websocket.Handler(func(conn *websocket.Conn) { websocket.Handler(func(conn *websocket.Conn) {
ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second) ctx, cancel := context.WithTimeout(r.Context(), time.Duration(d.timeout)*time.Second)
defer cancel() defer cancel()

View File

@@ -71,6 +71,7 @@ func GetOptionalParam[T any](logger mlogger.Logger, r *http.Request, key string,
vals := r.URL.Query() vals := r.URL.Query()
s := vals.Get(key) s := vals.Get(key)
if s == "" { if s == "" {
//nolint:nilnil // Missing optional query parameter is represented as (nil, nil).
return nil, nil return nil, nil
} }

View File

@@ -1,17 +1,17 @@
package mutil package mutil
import ( import (
"context"
"net/http" "net/http"
"testing" "testing"
"github.com/tech/sendico/pkg/mlogger"
"github.com/stretchr/testify/assert" "github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
"go.uber.org/zap" "go.uber.org/zap"
) )
func TestGetOptionalBoolParam(t *testing.T) { func TestGetOptionalBoolParam(t *testing.T) {
logger := mlogger.Logger(zap.NewNop()) logger := zap.NewNop()
tests := []struct { tests := []struct {
name string name string
@@ -47,7 +47,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil) req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
require.NoError(t, err) require.NoError(t, err)
result, err := GetOptionalBoolParam(logger, req, "param") result, err := GetOptionalBoolParam(logger, req, "param")
@@ -69,7 +69,7 @@ func TestGetOptionalBoolParam(t *testing.T) {
} }
func TestGetOptionalInt64Param(t *testing.T) { func TestGetOptionalInt64Param(t *testing.T) {
logger := mlogger.Logger(zap.NewNop()) logger := zap.NewNop()
tests := []struct { tests := []struct {
name string name string
@@ -111,7 +111,7 @@ func TestGetOptionalInt64Param(t *testing.T) {
for _, tt := range tests { for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) { t.Run(tt.name, func(t *testing.T) {
req, err := http.NewRequest("GET", "http://example.com"+tt.query, nil) req, err := http.NewRequestWithContext(context.Background(), "GET", "http://example.com"+tt.query, nil)
require.NoError(t, err) require.NoError(t, err)
result, err := GetOptionalInt64Param(logger, req, "param") result, err := GetOptionalInt64Param(logger, req, "param")

View File

@@ -114,7 +114,7 @@ func (a *AccountAPI) deleteAll(r *http.Request, account *model.Account, token *s
a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account)) a.logger.Warn("Failed to delete all data", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
return nil, err return nil, err
} }
return nil, nil return struct{}{}, nil
}); err != nil { }); err != nil {
a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account)) a.logger.Warn("Failed to execute delete transaction", zap.Error(err), mzap.StorableRef(&org), mzap.StorableRef(account))
return response.Auto(a.logger, a.Name(), err) return response.Auto(a.logger, a.Name(), err)

View File

@@ -120,11 +120,11 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
var user model.Account var user model.Account
err = a.db.Get(ctx, accountRef, &user) err = a.db.Get(ctx, accountRef, &user)
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
a.logger.Info("User not found for password reset", zap.String("account_ref", accountRef.Hex())) a.logger.Info("User not found for password reset", mzap.ObjRef("account_ref", accountRef))
return response.NotFound(a.logger, a.Name(), "User not found") return response.NotFound(a.logger, a.Name(), "User not found")
} }
if err != nil { if err != nil {
a.logger.Warn("Failed to get user for password reset", zap.Error(err), zap.String("account_ref", accountRef.Hex())) a.logger.Warn("Failed to get user for password reset", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return response.Auto(a.logger, a.Name(), err) return response.Auto(a.logger, a.Name(), err)
} }
@@ -140,7 +140,7 @@ func (a *AccountAPI) resetPassword(r *http.Request) http.HandlerFunc {
} }
if t.AccountRef != accountRef { if t.AccountRef != accountRef {
a.logger.Warn("Token account reference does not match request account reference", zap.String("token_account_ref", t.AccountRef.Hex()), zap.String("request_account_ref", accountRef.Hex())) a.logger.Warn("Token account reference does not match request account reference", mzap.ObjRef("token_account_ref", t.AccountRef), mzap.ObjRef("request_account_ref", accountRef))
return response.DataConflict(a.logger, a.Name(), "Token does not match account") return response.DataConflict(a.logger, a.Name(), "Token does not match account")
} }
@@ -192,5 +192,5 @@ func (a *AccountAPI) resetPasswordTransactionBody(ctx context.Context, user *mod
// Don't fail the transaction if token revocation fails, but log it // Don't fail the transaction if token revocation fails, but log it
} }
return nil, nil return struct{}{}, nil
} }

View File

@@ -133,12 +133,7 @@ func TestPasswordValidationLogic(t *testing.T) {
for _, password := range invalidPasswords { for _, password := range invalidPasswords {
t.Run(password, func(t *testing.T) { t.Run(password, func(t *testing.T) {
// Test that invalid passwords fail at least one requirement // Test that invalid passwords fail at least one requirement
isValid := true isValid := len(password) >= 8
// Check length
if len(password) < 8 {
isValid = false
}
// Check for digit // Check for digit
hasDigit := false hasDigit := false

View File

@@ -276,7 +276,7 @@ func (a *AccountAPI) grantAllPermissions(ctx context.Context, organizationRef bs
for resource, granted := range required { for resource, granted := range required {
if !granted { if !granted {
a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", string(resource))) a.logger.Warn("Required policy description not found for signup permissions", zap.String("resource", resource))
} }
} }
@@ -338,9 +338,6 @@ func (a *AccountAPI) openOrgLedgerAccount(ctx context.Context, org *model.Organi
return merrors.Internal("chain gateway default asset is not configured") return merrors.Internal("chain gateway default asset is not configured")
} }
// TODO: remove hardcode
currency := "RUB"
var describable *describablev1.Describable var describable *describablev1.Describable
name := strings.TrimSpace(sr.LedgerWallet.Name) name := strings.TrimSpace(sr.LedgerWallet.Name)
var description *string var description *string
@@ -357,26 +354,47 @@ func (a *AccountAPI) openOrgLedgerAccount(ctx context.Context, org *model.Organi
} }
} }
resp, err := a.ledgerClient.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{ currencies := []string{"RUB", "USDT"}
OrganizationRef: org.ID.Hex(), if chainTokenCurrency := strings.ToUpper(strings.TrimSpace(a.chainAsset.GetTokenSymbol())); chainTokenCurrency != "" {
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, currencies = append(currencies, chainTokenCurrency)
Currency: currency, }
Status: ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE,
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, seen := make(map[string]struct{}, len(currencies))
Metadata: map[string]string{ for _, currency := range currencies {
"source": "signup", currency = strings.ToUpper(strings.TrimSpace(currency))
"login": sr.Account.Login, if currency == "" {
}, continue
Describable: describable, }
}) if _, exists := seen[currency]; exists {
if err != nil { continue
a.logger.Warn("Failed to create ledger account for organization", zap.Error(err), mzap.StorableRef(org)) }
return err seen[currency] = struct{}{}
}
if resp == nil || resp.GetAccount() == nil || strings.TrimSpace(resp.GetAccount().GetLedgerAccountRef()) == "" { resp, err := a.ledgerClient.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{
return merrors.Internal("ledger returned empty account reference") OrganizationRef: org.ID.Hex(),
AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET,
Currency: currency,
Status: ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE,
Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING,
Metadata: map[string]string{
"source": "signup",
"login": sr.Account.Login,
},
Describable: describable,
})
if err != nil {
a.logger.Warn("Failed to create ledger account for organization", zap.Error(err), mzap.StorableRef(org), zap.String("currency", currency))
return err
}
if resp == nil || resp.GetAccount() == nil || strings.TrimSpace(resp.GetAccount().GetLedgerAccountRef()) == "" {
return merrors.Internal("ledger returned empty account reference")
}
a.logger.Info("Ledger account created for organization",
mzap.StorableRef(org),
zap.String("currency", currency),
zap.String("ledger_account_ref", resp.GetAccount().GetLedgerAccountRef()))
} }
a.logger.Info("Ledger account created for organization", mzap.StorableRef(org), zap.String("ledger_account_ref", resp.GetAccount().GetLedgerAccountRef()))
return nil return nil
} }

View File

@@ -126,7 +126,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
require.NoError(t, err) require.NoError(t, err)
// Create HTTP request // Create HTTP request
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBuffer(reqBody)) req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBuffer(reqBody))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
// Parse the request body // Parse the request body
@@ -162,7 +162,7 @@ func TestSignupHTTPSerialization(t *testing.T) {
t.Run("InvalidJSONRequest", func(t *testing.T) { t.Run("InvalidJSONRequest", func(t *testing.T) {
invalidJSON := `{"account": {"login": "test@example.com", "password": "invalid json structure` invalidJSON := `{"account": {"login": "test@example.com", "password": "invalid json structure`
req := httptest.NewRequest(http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON)) req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/signup", bytes.NewBufferString(invalidJSON))
req.Header.Set("Content-Type", "application/json") req.Header.Set("Content-Type", "application/json")
var parsedRequest srequest.Signup var parsedRequest srequest.Signup

View File

@@ -16,13 +16,13 @@ import (
) )
type stubLedgerAccountClient struct { type stubLedgerAccountClient struct {
createReq *ledgerv1.CreateAccountRequest createReqs []*ledgerv1.CreateAccountRequest
createResp *ledgerv1.CreateAccountResponse createResp *ledgerv1.CreateAccountResponse
createErr error createErr error
} }
func (s *stubLedgerAccountClient) CreateAccount(_ context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) { func (s *stubLedgerAccountClient) CreateAccount(_ context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) {
s.createReq = req s.createReqs = append(s.createReqs, req)
return s.createResp, s.createErr return s.createResp, s.createErr
} }
@@ -31,7 +31,7 @@ func (s *stubLedgerAccountClient) Close() error {
} }
func TestOpenOrgLedgerAccount(t *testing.T) { func TestOpenOrgLedgerAccount(t *testing.T) {
t.Run("creates operating ledger account", func(t *testing.T) { t.Run("creates operating ledger accounts for RUB and USDT", func(t *testing.T) {
desc := " Main org ledger account " desc := " Main org ledger account "
sr := &srequest.Signup{ sr := &srequest.Signup{
Account: model.AccountData{ Account: model.AccountData{
@@ -65,22 +65,26 @@ func TestOpenOrgLedgerAccount(t *testing.T) {
err := api.openOrgLedgerAccount(context.Background(), org, sr) err := api.openOrgLedgerAccount(context.Background(), org, sr)
assert.NoError(t, err) assert.NoError(t, err)
if assert.NotNil(t, ledgerStub.createReq) { if assert.Len(t, ledgerStub.createReqs, 2) {
assert.Equal(t, org.ID.Hex(), ledgerStub.createReq.GetOrganizationRef()) currencies := make([]string, 0, len(ledgerStub.createReqs))
assert.Equal(t, "RUB", ledgerStub.createReq.GetCurrency()) for _, req := range ledgerStub.createReqs {
assert.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, ledgerStub.createReq.GetAccountType()) currencies = append(currencies, req.GetCurrency())
assert.Equal(t, ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, ledgerStub.createReq.GetStatus()) assert.Equal(t, org.ID.Hex(), req.GetOrganizationRef())
assert.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, ledgerStub.createReq.GetRole()) assert.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, req.GetAccountType())
assert.Equal(t, map[string]string{ assert.Equal(t, ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, req.GetStatus())
"source": "signup", assert.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, req.GetRole())
"login": "owner@example.com", assert.Equal(t, map[string]string{
}, ledgerStub.createReq.GetMetadata()) "source": "signup",
if assert.NotNil(t, ledgerStub.createReq.GetDescribable()) { "login": "owner@example.com",
assert.Equal(t, "Primary Ledger", ledgerStub.createReq.GetDescribable().GetName()) }, req.GetMetadata())
if assert.NotNil(t, ledgerStub.createReq.GetDescribable().Description) { if assert.NotNil(t, req.GetDescribable()) {
assert.Equal(t, "Main org ledger account", ledgerStub.createReq.GetDescribable().GetDescription()) assert.Equal(t, "Primary Ledger", req.GetDescribable().GetName())
if assert.NotNil(t, req.GetDescribable().Description) {
assert.Equal(t, "Main org ledger account", req.GetDescribable().GetDescription())
}
} }
} }
assert.ElementsMatch(t, []string{"RUB", "USDT"}, currencies)
} }
}) })

View File

@@ -0,0 +1,47 @@
package callbacksimp
import (
"context"
"encoding/json"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.uber.org/zap"
)
func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to parse organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
var callback model.Callback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
mutation, err := a.normalizeAndPrepare(r.Context(), &callback, organizationRef, "", true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
if err := a.DB.Create(ctx, *account.GetID(), organizationRef, &callback); err != nil {
return nil, err
}
if err := a.applySigningSecretMutation(ctx, *account.GetID(), *callback.GetID(), mutation); err != nil {
return nil, err
}
return struct{}{}, nil
}); err != nil {
a.Logger.Warn("Failed to create callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&callback, accessToken, mutation.Generated, true)
}

View File

@@ -2,7 +2,7 @@ package callbacksimp
import ( import (
"context" "context"
"encoding/json" "errors"
"net/http" "net/http"
"net/url" "net/url"
"strings" "strings"
@@ -16,68 +16,10 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
type callbackWriteResponse struct { type signingSecretMutation struct {
AccessToken sresponse.TokenData `json:"accessToken"` SetSecretRef string
Callbacks []model.Callback `json:"callbacks"` Clear bool
GeneratedSigningSecret string `json:"generatedSigningSecret,omitempty"` Generated string
}
func (a *CallbacksAPI) create(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
organizationRef, err := a.Oph.GetRef(r)
if err != nil {
a.Logger.Warn("Failed to parse organization reference", zap.Error(err), mutil.PLog(a.Oph, r))
return response.BadReference(a.Logger, a.Name(), a.Oph.Name(), a.Oph.GetID(r), err)
}
var callback model.Callback
if err := json.NewDecoder(r.Body).Decode(&callback); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
generatedSecret, err := a.normalizeAndPrepare(r.Context(), &callback, organizationRef, true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if err := a.DB.Create(r.Context(), *account.GetID(), organizationRef, &callback); err != nil {
a.Logger.Warn("Failed to create callback", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&callback, accessToken, generatedSecret, true)
}
func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
var input model.Callback
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
callbackRef := *input.GetID()
if callbackRef.IsZero() {
return response.Auto(a.Logger, a.Name(), merrors.InvalidArgument("callback id is required", "id"))
}
var existing model.Callback
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &existing); err != nil {
a.Logger.Warn("Failed to fetch callback before update", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
mergeCallbackMutable(&existing, &input)
generatedSecret, err := a.normalizeAndPrepare(r.Context(), &existing, existing.OrganizationRef, true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if err := a.DB.Update(r.Context(), *account.GetID(), &existing); err != nil {
a.Logger.Warn("Failed to update callback", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&existing, accessToken, generatedSecret, false)
} }
func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc { func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
@@ -102,10 +44,8 @@ func (a *CallbacksAPI) rotateSecret(r *http.Request, account *model.Account, acc
a.Logger.Warn("Failed to rotate callback signing secret", zap.Error(err)) a.Logger.Warn("Failed to rotate callback signing secret", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err) return response.Auto(a.Logger, a.Name(), err)
} }
callback.RetryPolicy.SecretRef = secretRef if err := a.db.SetSigningSecretRef(r.Context(), *account.GetID(), callbackRef, secretRef); err != nil {
a.Logger.Warn("Failed to persist rotated callback signing secret reference", zap.Error(err))
if err := a.DB.Update(r.Context(), *account.GetID(), &callback); err != nil {
a.Logger.Warn("Failed to persist rotated callback secret reference", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err) return response.Auto(a.Logger, a.Name(), err)
} }
@@ -116,29 +56,25 @@ func (a *CallbacksAPI) normalizeAndPrepare(
ctx context.Context, ctx context.Context,
callback *model.Callback, callback *model.Callback,
organizationRef bson.ObjectID, organizationRef bson.ObjectID,
existingSecretRef string,
allowSecretGeneration bool, allowSecretGeneration bool,
) (string, error) { ) (signingSecretMutation, error) {
if callback == nil { if callback == nil {
return "", merrors.InvalidArgument("callback payload is required") return signingSecretMutation{}, merrors.InvalidArgument("callback payload is required")
} }
if organizationRef.IsZero() { if organizationRef.IsZero() {
return "", merrors.InvalidArgument("organization reference is required", "organizationRef") return signingSecretMutation{}, merrors.InvalidArgument("organization reference is required", "organizationRef")
} }
callback.SetOrganizationRef(organizationRef)
callback.Name = strings.TrimSpace(callback.Name) callback.Name = strings.TrimSpace(callback.Name)
callback.Description = trimDescription(callback.Description) callback.Description = trimDescription(callback.Description)
callback.ClientID = strings.TrimSpace(callback.ClientID)
if callback.ClientID == "" {
callback.ClientID = organizationRef.Hex()
}
callback.URL = strings.TrimSpace(callback.URL) callback.URL = strings.TrimSpace(callback.URL)
if callback.URL == "" { if callback.URL == "" {
return "", merrors.InvalidArgument("url is required", "url") return signingSecretMutation{}, merrors.InvalidArgument("url is required", "url")
} }
if err := validateCallbackURL(callback.URL); err != nil { if err := validateCallbackURL(callback.URL); err != nil {
return "", err return signingSecretMutation{}, err
} }
if callback.Name == "" { if callback.Name == "" {
callback.Name = callback.URL callback.Name = callback.URL
@@ -146,15 +82,15 @@ func (a *CallbacksAPI) normalizeAndPrepare(
status, err := normalizeStatus(callback.Status, a.config.DefaultStatus) status, err := normalizeStatus(callback.Status, a.config.DefaultStatus)
if err != nil { if err != nil {
return "", err return signingSecretMutation{}, err
} }
callback.Status = status callback.Status = status
callback.EventTypes = normalizeEventTypes(callback.EventTypes, a.config.DefaultEventTypes) callback.EventTypes = normalizeEventTypes(callback.EventTypes, a.config.DefaultEventTypes)
callback.RetryPolicy.MinDelayMS = defaultInt(callback.RetryPolicy.MinDelayMS, defaultRetryMinDelayMS) callback.RetryPolicy.Backoff.MinDelayMS = defaultInt(callback.RetryPolicy.Backoff.MinDelayMS, defaultRetryMinDelayMS)
callback.RetryPolicy.MaxDelayMS = defaultInt(callback.RetryPolicy.MaxDelayMS, defaultRetryMaxDelayMS) callback.RetryPolicy.Backoff.MaxDelayMS = defaultInt(callback.RetryPolicy.Backoff.MaxDelayMS, defaultRetryMaxDelayMS)
if callback.RetryPolicy.MaxDelayMS < callback.RetryPolicy.MinDelayMS { if callback.RetryPolicy.Backoff.MaxDelayMS < callback.RetryPolicy.Backoff.MinDelayMS {
callback.RetryPolicy.MaxDelayMS = callback.RetryPolicy.MinDelayMS callback.RetryPolicy.Backoff.MaxDelayMS = callback.RetryPolicy.Backoff.MinDelayMS
} }
callback.RetryPolicy.MaxAttempts = defaultInt(callback.RetryPolicy.MaxAttempts, defaultRetryMaxAttempts) callback.RetryPolicy.MaxAttempts = defaultInt(callback.RetryPolicy.MaxAttempts, defaultRetryMaxAttempts)
callback.RetryPolicy.RequestTimeoutMS = defaultInt(callback.RetryPolicy.RequestTimeoutMS, defaultRetryRequestTimeoutMS) callback.RetryPolicy.RequestTimeoutMS = defaultInt(callback.RetryPolicy.RequestTimeoutMS, defaultRetryRequestTimeoutMS)
@@ -162,36 +98,55 @@ func (a *CallbacksAPI) normalizeAndPrepare(
mode, err := normalizeSigningMode(callback.RetryPolicy.SigningMode) mode, err := normalizeSigningMode(callback.RetryPolicy.SigningMode)
if err != nil { if err != nil {
return "", err return signingSecretMutation{}, err
} }
callback.RetryPolicy.SigningMode = mode callback.RetryPolicy.SigningMode = mode
callback.RetryPolicy.SecretRef = strings.TrimSpace(callback.RetryPolicy.SecretRef) existingSecretRef = strings.TrimSpace(existingSecretRef)
switch callback.RetryPolicy.SigningMode { switch callback.RetryPolicy.SigningMode {
case model.CallbackSigningModeNone: case model.CallbackSigningModeNone:
callback.RetryPolicy.SecretRef = "" return signingSecretMutation{Clear: existingSecretRef != ""}, nil
return "", nil
case model.CallbackSigningModeHMACSHA256: case model.CallbackSigningModeHMACSHA256:
if callback.RetryPolicy.SecretRef != "" { if existingSecretRef != "" {
return "", nil return signingSecretMutation{SetSecretRef: existingSecretRef}, nil
} }
if !allowSecretGeneration { if !allowSecretGeneration {
return "", merrors.InvalidArgument("secretRef is required for hmac_sha256 callbacks", "retryPolicy.secretRef") return signingSecretMutation{}, merrors.InvalidArgument("signing secret is required for hmac_sha256 callbacks", "retryPolicy.signingMode")
} }
if callback.GetID().IsZero() { if callback.GetID().IsZero() {
callback.SetID(bson.NewObjectID()) callback.SetID(bson.NewObjectID())
} }
secretRef, generatedSecret, err := a.secrets.Provision(ctx, organizationRef, *callback.GetID()) secretRef, generatedSecret, err := a.secrets.Provision(ctx, organizationRef, *callback.GetID())
if err != nil { if err != nil {
return "", err return signingSecretMutation{}, err
} }
callback.RetryPolicy.SecretRef = secretRef return signingSecretMutation{SetSecretRef: secretRef, Generated: generatedSecret}, nil
return generatedSecret, nil
default: default:
return "", merrors.InvalidArgument("unsupported signing mode", "retryPolicy.signingMode") return signingSecretMutation{}, merrors.InvalidArgument("unsupported signing mode", "retryPolicy.signingMode")
} }
} }
func (a *CallbacksAPI) applySigningSecretMutation(
ctx context.Context,
accountRef,
callbackRef bson.ObjectID,
mutation signingSecretMutation,
) error {
if callbackRef.IsZero() {
return merrors.InvalidArgument("callback reference is required", "callbackRef")
}
if strings.TrimSpace(mutation.SetSecretRef) != "" {
return a.db.SetSigningSecretRef(ctx, accountRef, callbackRef, mutation.SetSecretRef)
}
if mutation.Clear {
err := a.db.ClearSigningSecretRef(ctx, accountRef, callbackRef)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
return err
}
}
return nil
}
func (a *CallbacksAPI) callbackResponse( func (a *CallbacksAPI) callbackResponse(
callback *model.Callback, callback *model.Callback,
accessToken *sresponse.TokenData, accessToken *sresponse.TokenData,
@@ -202,15 +157,7 @@ func (a *CallbacksAPI) callbackResponse(
return response.Internal(a.Logger, a.Name(), merrors.Internal("failed to build callback response")) return response.Internal(a.Logger, a.Name(), merrors.Internal("failed to build callback response"))
} }
resp := callbackWriteResponse{ return sresponse.Callback(a.Logger, callback, accessToken, generatedSecret, created)
AccessToken: *accessToken,
Callbacks: []model.Callback{*callback},
GeneratedSigningSecret: generatedSecret,
}
if created {
return response.Created(a.Logger, resp)
}
return response.Ok(a.Logger, resp)
} }
func normalizeStatus(raw, fallback model.CallbackStatus) (model.CallbackStatus, error) { func normalizeStatus(raw, fallback model.CallbackStatus) (model.CallbackStatus, error) {
@@ -286,16 +233,17 @@ func normalizeHeaders(headers map[string]string) map[string]string {
} }
func mergeCallbackMutable(dst, src *model.Callback) { func mergeCallbackMutable(dst, src *model.Callback) {
dst.OrganizationRef = src.OrganizationRef
dst.Describable = src.Describable dst.Describable = src.Describable
dst.ClientID = src.ClientID
dst.Status = src.Status dst.Status = src.Status
dst.URL = src.URL dst.URL = src.URL
dst.EventTypes = append([]string(nil), src.EventTypes...) dst.EventTypes = append([]string(nil), src.EventTypes...)
dst.RetryPolicy = model.CallbackRetryPolicy{ dst.RetryPolicy = model.CallbackRetryPolicy{
MinDelayMS: src.RetryPolicy.MinDelayMS, Backoff: model.CallbackBackoff{
MaxDelayMS: src.RetryPolicy.MaxDelayMS, MinDelayMS: src.RetryPolicy.Backoff.MinDelayMS,
MaxDelayMS: src.RetryPolicy.Backoff.MaxDelayMS,
},
SigningMode: src.RetryPolicy.SigningMode, SigningMode: src.RetryPolicy.SigningMode,
SecretRef: src.RetryPolicy.SecretRef,
Headers: normalizeHeaders(src.RetryPolicy.Headers), Headers: normalizeHeaders(src.RetryPolicy.Headers),
MaxAttempts: src.RetryPolicy.MaxAttempts, MaxAttempts: src.RetryPolicy.MaxAttempts,
RequestTimeoutMS: src.RetryPolicy.RequestTimeoutMS, RequestTimeoutMS: src.RetryPolicy.RequestTimeoutMS,

View File

@@ -13,6 +13,7 @@ import (
"github.com/prometheus/client_golang/prometheus/promauto" "github.com/prometheus/client_golang/prometheus/promauto"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/pkg/vault/kv" "github.com/tech/sendico/pkg/vault/kv"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
@@ -81,7 +82,7 @@ func newSigningSecretManager(logger mlogger.Logger, cfg callbacksConfig) (signin
} }
if isVaultConfigEmpty(cfg.Vault) { if isVaultConfigEmpty(cfg.Vault) {
manager.logger.Warn("Callbacks Vault config is not set; secret generation requires explicit secretRef in payloads") manager.logger.Warn("Callbacks Vault config is not set; hmac signing secret generation is disabled")
ensureSigningSecretMetrics() ensureSigningSecretMetrics()
return manager, nil return manager, nil
} }
@@ -152,7 +153,7 @@ func (m *vaultSigningSecretManager) Provision(
} }
secretRef := "vault:" + secretPath + "#" + m.field secretRef := "vault:" + secretPath + "#" + m.field
m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), zap.String("callback_ref", callbackRef.Hex())) m.logger.Info("Callback signing secret stored", zap.String("secret_ref", secretRef), mzap.ObjRef("callback_ref", callbackRef))
return secretRef, secret, nil return secretRef, secret, nil
} }

View File

@@ -5,6 +5,7 @@ import (
api "github.com/tech/sendico/pkg/api/http" api "github.com/tech/sendico/pkg/api/http"
"github.com/tech/sendico/pkg/db/callbacks" "github.com/tech/sendico/pkg/db/callbacks"
"github.com/tech/sendico/pkg/db/transaction"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
@@ -16,6 +17,7 @@ import (
type CallbacksAPI struct { type CallbacksAPI struct {
papitemplate.ProtectedAPI[model.Callback] papitemplate.ProtectedAPI[model.Callback]
db callbacks.DB db callbacks.DB
tf transaction.Factory
secrets signingSecretManager secrets signingSecretManager
config callbacksConfig config callbacksConfig
} }
@@ -35,6 +37,7 @@ func CreateAPI(apiCtx eapi.API) (*CallbacksAPI, error) {
res := &CallbacksAPI{ res := &CallbacksAPI{
config: newCallbacksConfig(apiCtx.Config().Callbacks), config: newCallbacksConfig(apiCtx.Config().Callbacks),
tf: apiCtx.DBFactory().TransactionFactory(),
} }
p, err := papitemplate.CreateAPI(apiCtx, dbFactory, mservice.Organizations, mservice.Callbacks) p, err := papitemplate.CreateAPI(apiCtx, dbFactory, mservice.Organizations, mservice.Callbacks)

View File

@@ -0,0 +1,59 @@
package callbacksimp
import (
"context"
"encoding/json"
"errors"
"net/http"
"github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/server/interface/api/sresponse"
"go.uber.org/zap"
)
func (a *CallbacksAPI) update(r *http.Request, account *model.Account, accessToken *sresponse.TokenData) http.HandlerFunc {
var input model.Callback
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
a.Logger.Warn("Failed to decode callback payload", zap.Error(err))
return response.BadPayload(a.Logger, a.Name(), err)
}
callbackRef := *input.GetID()
if callbackRef.IsZero() {
return response.Auto(a.Logger, a.Name(), merrors.InvalidArgument("callback ref is required", "id"))
}
var existing model.Callback
if err := a.db.Get(r.Context(), *account.GetID(), callbackRef, &existing); err != nil {
a.Logger.Warn("Failed to fetch callback before update", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
existingSecretRef, err := a.db.GetSigningSecretRef(r.Context(), *account.GetID(), callbackRef)
if err != nil && !errors.Is(err, merrors.ErrNoData) {
a.Logger.Warn("Failed to fetch callback signing secret metadata", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
mergeCallbackMutable(&existing, &input)
mutation, err := a.normalizeAndPrepare(r.Context(), &existing, existing.OrganizationRef, existingSecretRef, true)
if err != nil {
return response.Auto(a.Logger, a.Name(), err)
}
if _, err := a.tf.CreateTransaction().Execute(r.Context(), func(ctx context.Context) (any, error) {
if err := a.DB.Update(ctx, *account.GetID(), &existing); err != nil {
return nil, err
}
if err := a.applySigningSecretMutation(ctx, *account.GetID(), callbackRef, mutation); err != nil {
return nil, err
}
return struct{}{}, nil
}); err != nil {
a.Logger.Warn("Failed to update callback transaction", zap.Error(err))
return response.Auto(a.Logger, a.Name(), err)
}
return a.callbackResponse(&existing, accessToken, mutation.Generated, false)
}

View File

@@ -6,6 +6,7 @@ import (
"net/http" "net/http"
"os" "os"
"path/filepath" "path/filepath"
"strings"
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/domainprovider" "github.com/tech/sendico/pkg/domainprovider"
@@ -34,7 +35,10 @@ func (storage *LocalStorage) Delete(ctx context.Context, objID string) error {
default: default:
} }
filePath := filepath.Join(storage.storageDir, objID) filePath, err := storage.resolvePath(objID)
if err != nil {
return err
}
if err := os.Remove(filePath); err != nil { if err := os.Remove(filePath); err != nil {
if os.IsNotExist(err) { if os.IsNotExist(err) {
storage.logger.Debug("File not found", zap.String("obj_ref", objID)) storage.logger.Debug("File not found", zap.String("obj_ref", objID))
@@ -54,7 +58,11 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
default: default:
} }
filePath := filepath.Join(storage.storageDir, objID) filePath, err := storage.resolvePath(objID)
if err != nil {
return "", err
}
//nolint:gosec // File path is resolved and constrained to storage root.
dst, err := os.Create(filePath) dst, err := os.Create(filePath)
if err != nil { if err != nil {
storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID)) storage.logger.Warn("Error occurred while creating file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objID))
@@ -78,7 +86,9 @@ func (storage *LocalStorage) Save(ctx context.Context, file io.Reader, objID str
} }
case <-ctx.Done(): case <-ctx.Done():
// Context was cancelled, clean up the partial file // Context was cancelled, clean up the partial file
os.Remove(filePath) if removeErr := os.Remove(filePath); removeErr != nil && !os.IsNotExist(removeErr) {
storage.logger.Warn("Failed to remove partially written file", zap.Error(removeErr), zap.String("obj_ref", objID))
}
return "", ctx.Err() return "", ctx.Err()
} }
@@ -93,7 +103,10 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
default: default:
} }
filePath := filepath.Join(storage.storageDir, objRef) filePath, err := storage.resolvePath(objRef)
if err != nil {
return response.Internal(storage.logger, storage.service, err)
}
if _, err := os.Stat(filePath); err != nil { if _, err := os.Stat(filePath); err != nil {
storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef)) storage.logger.Warn("Failed to access file", zap.Error(err), zap.String("storage", storage.storageDir), zap.String("obj_ref", objRef))
return response.Internal(storage.logger, storage.service, err) return response.Internal(storage.logger, storage.service, err)
@@ -117,7 +130,7 @@ func (storage *LocalStorage) Get(ctx context.Context, objRef string) http.Handle
func ensureDir(dirName string) error { func ensureDir(dirName string) error {
info, err := os.Stat(dirName) info, err := os.Stat(dirName)
if os.IsNotExist(err) { if os.IsNotExist(err) {
return os.MkdirAll(dirName, 0o755) return os.MkdirAll(dirName, 0o750)
} }
if err != nil { if err != nil {
return err return err
@@ -128,6 +141,24 @@ func ensureDir(dirName string) error {
return nil return nil
} }
func (storage *LocalStorage) resolvePath(objID string) (string, error) {
objID = strings.TrimSpace(objID)
if objID == "" {
return "", merrors.InvalidArgument("obj_ref is required", "obj_ref")
}
filePath := filepath.Join(storage.storageDir, objID)
relPath, err := filepath.Rel(storage.storageDir, filePath)
if err != nil {
return "", merrors.InternalWrap(err, "failed to resolve local file path")
}
if relPath == "." || strings.HasPrefix(relPath, "..") {
return "", merrors.InvalidArgument("obj_ref is invalid", "obj_ref")
}
return filePath, nil
}
func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) { func CreateLocalFileStorage(logger mlogger.Logger, service mservice.Type, directory, subDir string, dp domainprovider.DomainProvider, cfg config.LocalFSSConfig) (*LocalStorage, error) {
dir := filepath.Join(cfg.RootPath, directory) dir := filepath.Join(cfg.RootPath, directory)
if err := ensureDir(dir); err != nil { if err := ensureDir(dir); err != nil {

View File

@@ -55,7 +55,7 @@ func setupTestStorage(t *testing.T) (*LocalStorage, string, func()) {
// Return cleanup function // Return cleanup function
cleanup := func() { cleanup := func() {
os.RemoveAll(tempDir) require.NoError(t, os.RemoveAll(tempDir))
} }
return storage, tempDir, cleanup return storage, tempDir, cleanup
@@ -81,7 +81,7 @@ func setupBenchmarkStorage(b *testing.B) (*LocalStorage, string, func()) {
// Return cleanup function // Return cleanup function
cleanup := func() { cleanup := func() {
os.RemoveAll(tempDir) require.NoError(b, os.RemoveAll(tempDir))
} }
return storage, tempDir, cleanup return storage, tempDir, cleanup
@@ -138,6 +138,7 @@ func TestLocalStorage_Save(t *testing.T) {
// Verify file was actually saved // Verify file was actually saved
filePath := filepath.Join(tempDir, tt.objID) filePath := filepath.Join(tempDir, tt.objID)
//nolint:gosec // Test-controlled path inside temporary directory.
content, err := os.ReadFile(filePath) content, err := os.ReadFile(filePath)
assert.NoError(t, err) assert.NoError(t, err)
assert.Equal(t, tt.content, string(content)) assert.Equal(t, tt.content, string(content))
@@ -186,7 +187,7 @@ func TestLocalStorage_Delete(t *testing.T) {
// Create a test file // Create a test file
testFile := filepath.Join(tempDir, "test.txt") testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644) err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err) require.NoError(t, err)
tests := []struct { tests := []struct {
@@ -232,7 +233,7 @@ func TestLocalStorage_Delete_ContextCancellation(t *testing.T) {
// Create a test file // Create a test file
testFile := filepath.Join(tempDir, "test.txt") testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644) err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Create a context that's already cancelled // Create a context that's already cancelled
@@ -256,7 +257,7 @@ func TestLocalStorage_Get(t *testing.T) {
// Create a test file // Create a test file
testContent := "test file content" testContent := "test file content"
testFile := filepath.Join(tempDir, "test.txt") testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte(testContent), 0o644) err := os.WriteFile(testFile, []byte(testContent), 0o600)
require.NoError(t, err) require.NoError(t, err)
tests := []struct { tests := []struct {
@@ -285,7 +286,7 @@ func TestLocalStorage_Get(t *testing.T) {
handler := storage.Get(ctx, tt.objID) handler := storage.Get(ctx, tt.objID)
// Create test request // Create test request
req := httptest.NewRequest("GET", "/files/"+tt.objID, nil) req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/"+tt.objID, nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
handler.ServeHTTP(w, req) handler.ServeHTTP(w, req)
@@ -304,7 +305,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
// Create a test file // Create a test file
testFile := filepath.Join(tempDir, "test.txt") testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644) err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err) require.NoError(t, err)
// Create a context that's already cancelled // Create a context that's already cancelled
@@ -314,7 +315,7 @@ func TestLocalStorage_Get_ContextCancellation(t *testing.T) {
handler := storage.Get(ctx, "test.txt") handler := storage.Get(ctx, "test.txt")
// Create test request // Create test request
req := httptest.NewRequest("GET", "/files/test.txt", nil) req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
w := httptest.NewRecorder() w := httptest.NewRecorder()
handler.ServeHTTP(w, req) handler.ServeHTTP(w, req)
@@ -328,14 +329,14 @@ func TestLocalStorage_Get_RequestContextCancellation(t *testing.T) {
// Create a test file // Create a test file
testFile := filepath.Join(tempDir, "test.txt") testFile := filepath.Join(tempDir, "test.txt")
err := os.WriteFile(testFile, []byte("test content"), 0o644) err := os.WriteFile(testFile, []byte("test content"), 0o600)
require.NoError(t, err) require.NoError(t, err)
ctx := context.Background() ctx := context.Background()
handler := storage.Get(ctx, "test.txt") handler := storage.Get(ctx, "test.txt")
// Create test request with cancelled context // Create test request with cancelled context
req := httptest.NewRequest("GET", "/files/test.txt", nil) req := httptest.NewRequestWithContext(context.Background(), "GET", "/files/test.txt", nil)
reqCtx, cancel := context.WithCancel(req.Context()) reqCtx, cancel := context.WithCancel(req.Context())
req = req.WithContext(reqCtx) req = req.WithContext(reqCtx)
cancel() // Cancel the request context cancel() // Cancel the request context
@@ -352,7 +353,9 @@ func TestCreateLocalFileStorage(t *testing.T) {
// Create temporary directory for testing // Create temporary directory for testing
tempDir, err := os.MkdirTemp("", "storage_test") tempDir, err := os.MkdirTemp("", "storage_test")
require.NoError(t, err) require.NoError(t, err)
defer os.RemoveAll(tempDir) defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
logger := zap.NewNop() logger := zap.NewNop()
cfg := config.LocalFSSConfig{ cfg := config.LocalFSSConfig{
@@ -372,10 +375,12 @@ func TestCreateLocalFileStorage_InvalidPath(t *testing.T) {
// Build a deterministic failure case: the target path already exists as a file. // Build a deterministic failure case: the target path already exists as a file.
tempDir, err := os.MkdirTemp("", "storage_invalid_path_test") tempDir, err := os.MkdirTemp("", "storage_invalid_path_test")
require.NoError(t, err) require.NoError(t, err)
defer os.RemoveAll(tempDir) defer func() {
require.NoError(t, os.RemoveAll(tempDir))
}()
fileAtTargetPath := filepath.Join(tempDir, "test") fileAtTargetPath := filepath.Join(tempDir, "test")
err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644) err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o600)
require.NoError(t, err) require.NoError(t, err)
logger := zap.NewNop() logger := zap.NewNop()
@@ -426,7 +431,7 @@ func TestLocalStorage_ConcurrentOperations(t *testing.T) {
// Create files to delete // Create files to delete
for i := 0; i < 5; i++ { for i := 0; i < 5; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i)) filePath := filepath.Join(tempDir, fmt.Sprintf("delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644) err := os.WriteFile(filePath, []byte("content"), 0o600)
require.NoError(t, err) require.NoError(t, err)
} }
@@ -536,7 +541,7 @@ func BenchmarkLocalStorage_Delete(b *testing.B) {
// Pre-create files for deletion // Pre-create files for deletion
for i := 0; i < b.N; i++ { for i := 0; i < b.N; i++ {
filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i)) filePath := filepath.Join(tempDir, fmt.Sprintf("bench_delete_%d.txt", i))
err := os.WriteFile(filePath, []byte("content"), 0o644) err := os.WriteFile(filePath, []byte("content"), 0o600)
if err != nil { if err != nil {
b.Fatal(err) b.Fatal(err)
} }

View File

@@ -11,6 +11,7 @@ import (
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/model/account_role" "github.com/tech/sendico/pkg/model/account_role"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1"
ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
@@ -88,7 +89,7 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
Describable: describable, Describable: describable,
}) })
if err != nil { if err != nil {
a.logger.Warn("Failed to create ledger account", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to create ledger account", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, mservice.Ledger, err) return response.Auto(a.logger, mservice.Ledger, err)
} }
@@ -96,7 +97,9 @@ func (a *LedgerAPI) createAccount(r *http.Request, account *model.Account, token
} }
func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) { func decodeLedgerAccountCreatePayload(r *http.Request) (*srequest.CreateLedgerAccount, error) {
defer r.Body.Close() defer func() {
_ = r.Body.Close()
}()
payload := srequest.CreateLedgerAccount{} payload := srequest.CreateLedgerAccount{}
if err := json.NewDecoder(r.Body).Decode(&payload); err != nil { if err := json.NewDecoder(r.Body).Decode(&payload); err != nil {

View File

@@ -47,7 +47,7 @@ func (a *LedgerAPI) listAccounts(r *http.Request, account *model.Account, token
resp, err := a.client.ListAccounts(ctx, req) resp, err := a.client.ListAccounts(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to list ledger accounts", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to list ledger accounts", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return response.Auto(a.logger, mservice.Ledger, err) return response.Auto(a.logger, mservice.Ledger, err)
} }

View File

@@ -46,7 +46,7 @@ func (a *ProtectedAPI[T]) archive(r *http.Request, account *model.Account, acces
ctx := r.Context() ctx := r.Context()
_, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) { _, err = a.a.DBFactory().TransactionFactory().CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) {
return nil, a.DB.SetArchived(r.Context(), *account.GetID(), organizationRef, objectRef, *archived, *cascade) return nil, a.DB.SetArchived(ctx, *account.GetID(), organizationRef, objectRef, *archived, *cascade)
}) })
if err != nil { if err != nil {
a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r), a.Logger.Warn("Failed to change archive property", zap.Error(err), mzap.StorableRef(account), mutil.PLog(a.Cph, r),

View File

@@ -15,6 +15,9 @@ import (
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap" "github.com/tech/sendico/pkg/mutil/mzap"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1" documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param" mutil "github.com/tech/sendico/server/internal/mutil/param"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
@@ -28,38 +31,99 @@ import (
const ( const (
documentsServiceName = "BILLING_DOCUMENTS" documentsServiceName = "BILLING_DOCUMENTS"
documentsOperationGet = discovery.OperationDocumentsGet documentsOperationGet = discovery.OperationDocumentsGet
documentsDialTimeout = 5 * time.Second
documentsCallTimeout = 10 * time.Second documentsCallTimeout = 10 * time.Second
gatewayCallTimeout = 10 * time.Second
) )
func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc { var allowedOperationGatewayServices = map[mservice.Type]struct{}{
mservice.ChainGateway: {},
mservice.TronGateway: {},
mservice.MntxGateway: {},
mservice.PaymentGateway: {},
mservice.TgSettle: {},
}
func (a *PaymentAPI) getOperationDocument(r *http.Request, account *model.Account, _ *sresponse.TokenData) http.HandlerFunc {
orgRef, denied := a.authorizeDocumentDownload(r, account)
if denied != nil {
return denied
}
query := r.URL.Query()
gatewayService := normalizeGatewayService(query.Get("gateway_service"))
if gatewayService == "" {
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "gateway_service is required")
}
if _, ok := allowedOperationGatewayServices[gatewayService]; !ok {
return response.BadRequest(a.logger, a.Name(), "invalid_parameter", "unsupported gateway_service")
}
operationRef := strings.TrimSpace(query.Get("operation_ref"))
if operationRef == "" {
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "operation_ref is required")
}
paymentRef := strings.TrimSpace(query.Get("payment_ref"))
if paymentRef == "" {
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "payment_ref is required")
}
service, gateway, h := a.resolveOperationDocumentDeps(r.Context(), gatewayService)
if h != nil {
return h
}
op, err := a.fetchGatewayOperation(r.Context(), gateway.InvokeURI, operationRef)
if err != nil {
a.logger.Warn("Failed to fetch gateway operation for document generation", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
return documentErrorResponse(a.logger, a.Name(), err)
}
req := operationDocumentRequest(orgRef.Hex(), gatewayService, operationRef, paymentRef, op)
if payment, paymentErr := a.fetchPayment(r.Context(), orgRef.Hex(), paymentRef); paymentErr != nil {
a.logger.Warn(
"Failed to fetch payment snapshot for operation document",
zap.Error(paymentErr),
mzap.ObjRef("organization_ref", orgRef),
zap.String("payment_ref", paymentRef),
zap.String("operation_ref", operationRef),
)
} else {
enrichOperationDocumentRequestFromPayment(req, payment)
}
docResp, err := a.fetchOperationDocument(r.Context(), service.InvokeURI, req)
if err != nil {
a.logger.Warn("Failed to fetch operation document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef), zap.String("gateway_service", gatewayService), zap.String("operation_ref", operationRef))
return documentErrorResponse(a.logger, a.Name(), err)
}
return operationDocumentResponse(a.logger, a.Name(), docResp, fmt.Sprintf("operation_%s.pdf", sanitizeFilenameComponent(operationRef)))
}
func (a *PaymentAPI) authorizeDocumentDownload(r *http.Request, account *model.Account) (bson.ObjectID, http.HandlerFunc) {
orgRef, err := a.oph.GetRef(r) orgRef, err := a.oph.GetRef(r)
if err != nil { if err != nil {
a.logger.Warn("Failed to parse organization reference for document request", zap.Error(err), mutil.PLog(a.oph, r)) a.logger.Warn("Failed to parse organization reference for document request", zap.Error(err), mutil.PLog(a.oph, r))
return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err) return bson.NilObjectID, response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err)
} }
ctx := r.Context() ctx := r.Context()
allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionRead) allowed, err := a.enf.Enforce(ctx, a.permissionRef, account.ID, orgRef, bson.NilObjectID, model.ActionRead)
if err != nil { if err != nil {
a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r)) a.logger.Warn("Failed to check payments access permissions", zap.Error(err), mutil.PLog(a.oph, r))
return response.Auto(a.logger, a.Name(), err) return bson.NilObjectID, response.Auto(a.logger, a.Name(), err)
} }
if !allowed { if !allowed {
a.logger.Debug("Access denied when downloading act", mutil.PLog(a.oph, r)) a.logger.Debug("Access denied when downloading document", mutil.PLog(a.oph, r))
return response.AccessDenied(a.logger, a.Name(), "payments read permission denied") return bson.NilObjectID, response.AccessDenied(a.logger, a.Name(), "payments read permission denied")
} }
paymentRef := strings.TrimSpace(r.URL.Query().Get("payment_ref")) return orgRef, nil
if paymentRef == "" { }
paymentRef = strings.TrimSpace(r.URL.Query().Get("paymentRef"))
}
if paymentRef == "" {
return response.BadRequest(a.logger, a.Name(), "missing_parameter", "payment_ref is required")
}
func (a *PaymentAPI) resolveOperationDocumentDeps(ctx context.Context, gatewayService mservice.Type) (*discovery.ServiceSummary, *discovery.GatewaySummary, http.HandlerFunc) {
if a.discovery == nil { if a.discovery == nil {
return response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "discovery client is not configured") return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "discovery client is not configured")
} }
lookupCtx, cancel := context.WithTimeout(ctx, discoveryLookupTimeout) lookupCtx, cancel := context.WithTimeout(ctx, discoveryLookupTimeout)
@@ -68,27 +132,35 @@ func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *
lookupResp, err := a.discovery.Lookup(lookupCtx) lookupResp, err := a.discovery.Lookup(lookupCtx)
if err != nil { if err != nil {
a.logger.Warn("Failed to lookup discovery registry", zap.Error(err)) a.logger.Warn("Failed to lookup discovery registry", zap.Error(err))
return response.Auto(a.logger, a.Name(), err) return nil, nil, response.Auto(a.logger, a.Name(), err)
} }
service := findDocumentsService(lookupResp.Services) service := findDocumentsService(lookupResp.Services)
if service == nil { if service == nil {
return response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "billing documents service unavailable") return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "billing documents service unavailable")
} }
docResp, err := a.fetchActDocument(ctx, service.InvokeURI, paymentRef) gateway := findGatewayForService(lookupResp.Gateways, gatewayService)
if err != nil { if gateway == nil {
a.logger.Warn("Failed to fetch act document", zap.Error(err), mzap.ObjRef("organization_ref", orgRef)) return nil, nil, response.Error(a.logger, a.Name(), http.StatusServiceUnavailable, "service_unavailable", "gateway service unavailable")
return documentErrorResponse(a.logger, a.Name(), err)
} }
if len(docResp.GetContent()) == 0 {
return response.Error(a.logger, a.Name(), http.StatusInternalServerError, "empty_document", "document service returned empty payload") return service, gateway, nil
}
func operationDocumentResponse(logger mlogger.Logger, source mservice.Type, docResp *documentsv1.GetDocumentResponse, fallbackFilename string) http.HandlerFunc {
if docResp == nil || len(docResp.GetContent()) == 0 {
return response.Error(logger, source, http.StatusInternalServerError, "empty_document", "document service returned empty payload")
} }
filename := strings.TrimSpace(docResp.GetFilename()) filename := strings.TrimSpace(docResp.GetFilename())
if filename == "" { if filename == "" {
filename = fmt.Sprintf("act_%s.pdf", paymentRef) filename = strings.TrimSpace(fallbackFilename)
} }
if filename == "" {
filename = "document.pdf"
}
mimeType := strings.TrimSpace(docResp.GetMimeType()) mimeType := strings.TrimSpace(docResp.GetMimeType())
if mimeType == "" { if mimeType == "" {
mimeType = "application/pdf" mimeType = "application/pdf"
@@ -98,31 +170,295 @@ func (a *PaymentAPI) getActDocument(r *http.Request, account *model.Account, _ *
w.Header().Set("Content-Type", mimeType) w.Header().Set("Content-Type", mimeType)
w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename)) w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%q", filename))
w.WriteHeader(http.StatusOK) w.WriteHeader(http.StatusOK)
if _, writeErr := w.Write(docResp.GetContent()); writeErr != nil { //nolint:gosec // Binary payload is served as attachment with explicit content type.
a.logger.Warn("Failed to write document response", zap.Error(writeErr)) if _, err := w.Write(docResp.GetContent()); err != nil {
logger.Warn("Failed to write document response", zap.Error(err))
} }
} }
} }
func (a *PaymentAPI) fetchActDocument(ctx context.Context, invokeURI, paymentRef string) (*documentsv1.GetDocumentResponse, error) { func normalizeGatewayService(raw string) mservice.Type {
dialCtx, cancel := context.WithTimeout(ctx, documentsDialTimeout) value := strings.ToLower(strings.TrimSpace(raw))
defer cancel() if value == "" {
return ""
}
conn, err := grpc.DialContext(dialCtx, invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials())) switch value {
case mservice.ChainGateway:
return mservice.ChainGateway
case mservice.TronGateway:
return mservice.TronGateway
case mservice.MntxGateway:
return mservice.MntxGateway
case mservice.PaymentGateway:
return mservice.PaymentGateway
case mservice.TgSettle:
return mservice.TgSettle
default:
return ""
}
}
func sanitizeFilenameComponent(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return ""
}
var b strings.Builder
b.Grow(len(trimmed))
for _, r := range trimmed {
switch {
case r >= 'a' && r <= 'z':
b.WriteRune(r)
case r >= 'A' && r <= 'Z':
b.WriteRune(r)
case r >= '0' && r <= '9':
b.WriteRune(r)
case r == '-', r == '_':
b.WriteRune(r)
default:
b.WriteRune('_')
}
}
clean := strings.Trim(b.String(), "_")
if clean == "" {
return "operation"
}
return clean
}
func (a *PaymentAPI) fetchOperationDocument(ctx context.Context, invokeURI string, req *documentsv1.GetOperationDocumentRequest) (*documentsv1.GetDocumentResponse, error) {
conn, err := grpc.NewClient(invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
return nil, merrors.InternalWrap(err, "dial billing documents") return nil, merrors.InternalWrap(err, "dial billing documents")
} }
defer conn.Close() defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close billing documents connection", zap.Error(closeErr))
}
}()
client := documentsv1.NewDocumentServiceClient(conn) client := documentsv1.NewDocumentServiceClient(conn)
callCtx, callCancel := context.WithTimeout(ctx, documentsCallTimeout) callCtx, callCancel := context.WithTimeout(ctx, documentsCallTimeout)
defer callCancel() defer callCancel()
return client.GetDocument(callCtx, &documentsv1.GetDocumentRequest{ return client.GetOperationDocument(callCtx, req)
PaymentRef: paymentRef, }
Type: documentsv1.DocumentType_DOCUMENT_TYPE_ACT,
func (a *PaymentAPI) fetchGatewayOperation(ctx context.Context, invokeURI, operationRef string) (*connectorv1.Operation, error) {
conn, err := grpc.NewClient(invokeURI, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway connector")
}
defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connector connection", zap.Error(closeErr))
}
}()
client := connectorv1.NewConnectorServiceClient(conn)
callCtx, callCancel := context.WithTimeout(ctx, gatewayCallTimeout)
defer callCancel()
resp, err := client.GetOperation(callCtx, &connectorv1.GetOperationRequest{OperationId: strings.TrimSpace(operationRef)})
if err != nil {
return nil, err
}
op := resp.GetOperation()
if op == nil {
return nil, merrors.NoData("gateway returned empty operation payload")
}
return op, nil
}
func (a *PaymentAPI) fetchPayment(ctx context.Context, organizationRef, paymentRef string) (*orchestrationv2.Payment, error) {
if a.execution == nil {
return nil, merrors.Internal("payment execution client is not configured")
}
resp, err := a.execution.GetPayment(ctx, &orchestrationv2.GetPaymentRequest{
Meta: requestMeta(organizationRef, ""),
PaymentRef: strings.TrimSpace(paymentRef),
}) })
if err != nil {
return nil, err
}
if resp == nil {
return nil, merrors.NoData("payment orchestrator returned empty response")
}
if resp.GetPayment() == nil {
return nil, merrors.NoData("payment orchestrator returned empty payment")
}
return resp.GetPayment(), nil
}
func findGatewayForService(gateways []discovery.GatewaySummary, gatewayService mservice.Type) *discovery.GatewaySummary {
candidates := make([]discovery.GatewaySummary, 0, len(gateways))
for _, gw := range gateways {
if !gw.Healthy || strings.TrimSpace(gw.InvokeURI) == "" {
continue
}
rail := discovery.NormalizeRail(gw.Rail)
network := strings.ToLower(strings.TrimSpace(gw.Network))
switch gatewayService {
case mservice.MntxGateway:
if rail == discovery.NormalizeRail(discovery.RailCardPayout) {
candidates = append(candidates, gw)
}
case mservice.PaymentGateway, mservice.TgSettle:
if rail == discovery.NormalizeRail(discovery.RailProviderSettlement) {
candidates = append(candidates, gw)
}
case mservice.TronGateway:
if rail == discovery.NormalizeRail(discovery.RailCrypto) && strings.Contains(network, "tron") {
candidates = append(candidates, gw)
}
case mservice.ChainGateway:
if rail == discovery.NormalizeRail(discovery.RailCrypto) && !strings.Contains(network, "tron") {
candidates = append(candidates, gw)
}
}
}
if len(candidates) == 0 && gatewayService == mservice.ChainGateway {
for _, gw := range gateways {
if gw.Healthy && strings.TrimSpace(gw.InvokeURI) != "" && discovery.NormalizeRail(gw.Rail) == discovery.NormalizeRail(discovery.RailCrypto) {
candidates = append(candidates, gw)
}
}
}
if len(candidates) == 0 {
return nil
}
best := candidates[0]
for _, candidate := range candidates[1:] {
if candidate.RoutingPriority > best.RoutingPriority {
best = candidate
}
}
return &best
}
func operationDocumentRequest(organizationRef string, gatewayService mservice.Type, requestedOperationRef string, paymentRef string, op *connectorv1.Operation) *documentsv1.GetOperationDocumentRequest {
req := &documentsv1.GetOperationDocumentRequest{
OrganizationRef: strings.TrimSpace(organizationRef),
GatewayService: gatewayService,
OperationRef: firstNonEmpty(strings.TrimSpace(op.GetOperationRef()), strings.TrimSpace(requestedOperationRef)),
PaymentRef: strings.TrimSpace(paymentRef),
OperationCode: strings.TrimSpace(op.GetType().String()),
OperationLabel: strings.TrimSpace(op.GetType().String()),
OperationState: strings.TrimSpace(op.GetStatus().String()),
Amount: strings.TrimSpace(op.GetMoney().GetAmount()),
Currency: strings.TrimSpace(op.GetMoney().GetCurrency()),
}
if ts := op.GetCreatedAt(); ts != nil {
req.StartedAtUnixMs = ts.AsTime().UnixMilli()
}
if ts := op.GetUpdatedAt(); ts != nil {
req.CompletedAtUnixMs = ts.AsTime().UnixMilli()
}
if isFailedOperationStatus(op.GetStatus()) {
req.FailureCode = strings.TrimSpace(op.GetStatus().String())
}
return req
}
func isFailedOperationStatus(status connectorv1.OperationStatus) bool {
return status == connectorv1.OperationStatus_OPERATION_FAILED || status == connectorv1.OperationStatus_OPERATION_CANCELLED
}
func enrichOperationDocumentRequestFromPayment(req *documentsv1.GetOperationDocumentRequest, payment *orchestrationv2.Payment) {
if req == nil || payment == nil {
return
}
req.PaymentRef = firstNonEmpty(strings.TrimSpace(req.GetPaymentRef()), strings.TrimSpace(payment.GetPaymentRef()))
req.ClientName = firstNonEmpty(strings.TrimSpace(req.GetClientName()), paymentClientName(payment))
}
func paymentClientName(payment *orchestrationv2.Payment) string {
if payment == nil {
return ""
}
intent := payment.GetIntentSnapshot()
if intent == nil {
return ""
}
if customerDescription := strings.TrimSpace(intent.GetComment()); customerDescription != "" {
return customerDescription
}
return firstNonEmpty(
paymentEndpointClientName(intent.GetDestination()),
paymentEndpointClientName(intent.GetSource()),
)
}
func paymentEndpointClientName(endpoint *endpointv1.PaymentEndpoint) string {
if endpoint == nil {
return ""
}
method := endpoint.GetPaymentMethod()
if method == nil {
return ""
}
switch method.GetType() {
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD:
type cardMethodData struct {
FirstName string `bson:"firstName"`
LastName string `bson:"lastName"`
}
var payload cardMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return ""
}
return strings.TrimSpace(strings.Join([]string{
strings.TrimSpace(payload.FirstName),
strings.TrimSpace(payload.LastName),
}, " "))
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT:
type bankMethodData struct {
RecipientName string `bson:"recipientName"`
}
var payload bankMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return ""
}
return strings.TrimSpace(payload.RecipientName)
case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN:
type ibanMethodData struct {
AccountHolder string `bson:"accountHolder"`
}
var payload ibanMethodData
if err := bson.Unmarshal(method.GetData(), &payload); err != nil {
return ""
}
return strings.TrimSpace(payload.AccountHolder)
default:
return ""
}
} }
func findDocumentsService(services []discovery.ServiceSummary) *discovery.ServiceSummary { func findDocumentsService(services []discovery.ServiceSummary) *discovery.ServiceSummary {

View File

@@ -0,0 +1,120 @@
package paymentapiimp
import (
"testing"
"github.com/tech/sendico/pkg/mservice"
documentsv1 "github.com/tech/sendico/pkg/proto/billing/documents/v1"
moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
"go.mongodb.org/mongo-driver/v2/bson"
)
func TestOperationDocumentRequest_UsesStructuredOperationFieldsOnly(t *testing.T) {
op := &connectorv1.Operation{
OperationRef: "pay-123:hop_1",
Type: connectorv1.OperationType_TRANSFER,
Status: connectorv1.OperationStatus_OPERATION_SUCCESS,
Money: &moneyv1.Money{
Amount: "100.50",
Currency: "USDT",
},
}
req := operationDocumentRequest("org-1", mservice.ChainGateway, "requested-op", "pay-123", op)
if req == nil {
t.Fatalf("expected request")
}
if got, want := req.GetPaymentRef(), "pay-123"; got != want {
t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
}
if got := req.GetClientName(); got != "" {
t.Fatalf("expected empty client_name from operation-only request, got=%q", got)
}
if got := req.GetClientAddress(); got != "" {
t.Fatalf("expected empty client_address from operation-only request, got=%q", got)
}
}
func TestOperationDocumentRequest_FailureCodeFromStructuredStatus(t *testing.T) {
req := operationDocumentRequest("org-1", mservice.ChainGateway, "op", "pay-123", &connectorv1.Operation{
OperationRef: "pay-123:hop_1",
Type: connectorv1.OperationType_TRANSFER,
Status: connectorv1.OperationStatus_OPERATION_FAILED,
})
if req == nil {
t.Fatalf("expected request")
}
if got, want := req.GetFailureCode(), "OPERATION_FAILED"; got != want {
t.Fatalf("failure_code mismatch: got=%q want=%q", got, want)
}
}
func TestPaymentClientName_FromIntentComment(t *testing.T) {
payment := &orchestrationv2.Payment{
IntentSnapshot: &quotationv2.QuoteIntent{
Comment: "Jane Customer",
},
}
if got, want := paymentClientName(payment), "Jane Customer"; got != want {
t.Fatalf("paymentClientName mismatch: got=%q want=%q", got, want)
}
}
func TestPaymentClientName_FromStructuredCardEndpoint(t *testing.T) {
raw, err := bson.Marshal(map[string]string{
"firstName": "Jane",
"lastName": "Doe",
})
if err != nil {
t.Fatalf("Marshal: %v", err)
}
payment := &orchestrationv2.Payment{
IntentSnapshot: &quotationv2.QuoteIntent{
Destination: &endpointv1.PaymentEndpoint{
Source: &endpointv1.PaymentEndpoint_PaymentMethod{
PaymentMethod: &endpointv1.PaymentMethod{
Type: endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD,
Data: raw,
},
},
},
},
}
if got, want := paymentClientName(payment), "Jane Doe"; got != want {
t.Fatalf("paymentClientName mismatch: got=%q want=%q", got, want)
}
}
func TestEnrichOperationDocumentRequestFromPayment_SetsClientName(t *testing.T) {
req := &documentsv1.GetOperationDocumentRequest{
OperationRef: "pay-123:hop_1",
}
payment := &orchestrationv2.Payment{
PaymentRef: "pay-123",
IntentSnapshot: &quotationv2.QuoteIntent{
Comment: "Client Name",
},
}
enrichOperationDocumentRequestFromPayment(req, payment)
if got, want := req.GetPaymentRef(), "pay-123"; got != want {
t.Fatalf("payment_ref mismatch: got=%q want=%q", got, want)
}
if got, want := req.GetClientName(), "Client Name"; got != want {
t.Fatalf("client_name mismatch: got=%q want=%q", got, want)
}
}
func TestOperationDocumentRequest_Compatibility(t *testing.T) {
var _ = operationDocumentRequest("org-1", mservice.ChainGateway, "op", "pay-1", &connectorv1.Operation{})
}

View File

@@ -103,6 +103,7 @@ func listPaymentsPage(r *http.Request) (*paginationv1.CursorPageRequest, error)
} }
if cursor == "" && !hasLimit { if cursor == "" && !hasLimit {
//nolint:nilnil // Absent pagination params mean no page request should be sent downstream.
return nil, nil return nil, nil
} }
@@ -189,6 +190,7 @@ func firstNonEmpty(values ...string) string {
func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) { func parseRFC3339Timestamp(raw string, field string) (*timestamppb.Timestamp, error) {
trimmed := strings.TrimSpace(raw) trimmed := strings.TrimSpace(raw)
if trimmed == "" { if trimmed == "" {
//nolint:nilnil // Empty timestamp filter is represented as (nil, nil).
return nil, nil return nil, nil
} }
parsed, err := time.Parse(time.RFC3339, trimmed) parsed, err := time.Parse(time.RFC3339, trimmed)

View File

@@ -14,6 +14,7 @@ import (
chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1"
endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1" endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
@@ -59,25 +60,36 @@ func mapQuoteIntent(intent *srequest.PaymentIntent) (*quotationv2.QuoteIntent, e
SettlementMode: resolvedSettlementMode, SettlementMode: resolvedSettlementMode,
FeeTreatment: resolvedFeeTreatment, FeeTreatment: resolvedFeeTreatment,
SettlementCurrency: settlementCurrency, SettlementCurrency: settlementCurrency,
FxSide: mapFXSide(intent), Fx: mapFXIntent(intent),
} Comment: strings.TrimSpace(intent.Comment),
if comment := strings.TrimSpace(intent.Attributes["comment"]); comment != "" {
quoteIntent.Comment = comment
} }
return quoteIntent, nil return quoteIntent, nil
} }
func mapFXSide(intent *srequest.PaymentIntent) fxv1.Side { func mapFXIntent(intent *srequest.PaymentIntent) *sharedv1.FXIntent {
if intent == nil || intent.FX == nil { if intent == nil || intent.FX == nil || intent.FX.Pair == nil {
return fxv1.Side_SIDE_UNSPECIFIED return nil
} }
side := fxv1.Side_SIDE_UNSPECIFIED
switch strings.TrimSpace(string(intent.FX.Side)) { switch strings.TrimSpace(string(intent.FX.Side)) {
case string(srequest.FXSideBuyBaseSellQuote): case string(srequest.FXSideBuyBaseSellQuote):
return fxv1.Side_BUY_BASE_SELL_QUOTE side = fxv1.Side_BUY_BASE_SELL_QUOTE
case string(srequest.FXSideSellBaseBuyQuote): case string(srequest.FXSideSellBaseBuyQuote):
return fxv1.Side_SELL_BASE_BUY_QUOTE side = fxv1.Side_SELL_BASE_BUY_QUOTE
default: }
return fxv1.Side_SIDE_UNSPECIFIED if side == fxv1.Side_SIDE_UNSPECIFIED {
side = fxv1.Side_SELL_BASE_BUY_QUOTE
}
return &sharedv1.FXIntent{
Pair: &fxv1.CurrencyPair{
Base: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Base)),
Quote: strings.ToUpper(strings.TrimSpace(intent.FX.Pair.Quote)),
},
Side: side,
Firm: intent.FX.Firm,
TtlMs: intent.FX.TTLms,
PreferredProvider: strings.TrimSpace(intent.FX.PreferredProvider),
MaxAgeMs: intent.FX.MaxAgeMs,
} }
} }

View File

@@ -202,8 +202,14 @@ func TestMapQuoteIntent_DerivesSettlementCurrencyFromFX(t *testing.T) {
if got.GetSettlementCurrency() != "RUB" { if got.GetSettlementCurrency() != "RUB" {
t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency()) t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency())
} }
if got.GetFxSide() != fxv1.Side_SELL_BASE_BUY_QUOTE { if got.GetFx() == nil || got.GetFx().GetPair() == nil {
t.Fatalf("unexpected fx_side: got=%s", got.GetFxSide().String()) t.Fatalf("expected fx intent")
}
if got.GetFx().GetSide() != fxv1.Side_SELL_BASE_BUY_QUOTE {
t.Fatalf("unexpected fx side: got=%s", got.GetFx().GetSide().String())
}
if got.GetFx().GetPair().GetBase() != "USDT" || got.GetFx().GetPair().GetQuote() != "RUB" {
t.Fatalf("unexpected fx pair: got=%s/%s", got.GetFx().GetPair().GetBase(), got.GetFx().GetPair().GetQuote())
} }
} }
@@ -246,8 +252,11 @@ func TestMapQuoteIntent_PropagatesFXSideBuyBaseSellQuote(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("unexpected error: %v", err) t.Fatalf("unexpected error: %v", err)
} }
if got.GetFxSide() != fxv1.Side_BUY_BASE_SELL_QUOTE { if got.GetFx() == nil || got.GetFx().GetPair() == nil {
t.Fatalf("unexpected fx_side: got=%s", got.GetFxSide().String()) t.Fatalf("expected fx intent")
}
if got.GetFx().GetSide() != fxv1.Side_BUY_BASE_SELL_QUOTE {
t.Fatalf("unexpected fx side: got=%s", got.GetFx().GetSide().String())
} }
if got.GetSettlementCurrency() != "RUB" { if got.GetSettlementCurrency() != "RUB" {
t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency()) t.Fatalf("unexpected settlement currency: got=%q", got.GetSettlementCurrency())

View File

@@ -89,7 +89,7 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
req := &orchestrationv2.ExecutePaymentRequest{ req := &orchestrationv2.ExecutePaymentRequest{
Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey), Meta: requestMeta(orgRef.Hex(), payload.IdempotencyKey),
QuotationRef: quotationRef, QuotationRef: quotationRef,
ClientPaymentRef: metadataValue(payload.Metadata, "client_payment_ref"), ClientPaymentRef: strings.TrimSpace(payload.ClientPaymentRef),
} }
resp, err := a.execution.ExecutePayment(ctx, req) resp, err := a.execution.ExecutePayment(ctx, req)
@@ -102,7 +102,9 @@ func (a *PaymentAPI) initiatePayment(r *http.Request, account *model.Account, to
} }
func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) { func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
defer r.Body.Close() defer func() {
_ = r.Body.Close()
}()
payload := &srequest.InitiatePayment{} payload := &srequest.InitiatePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil { if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
@@ -110,6 +112,7 @@ func decodeInitiatePayload(r *http.Request) (*srequest.InitiatePayment, error) {
} }
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey) payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef) payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
payload.ClientPaymentRef = strings.TrimSpace(payload.ClientPaymentRef)
if err := payload.Validate(); err != nil { if err := payload.Validate(); err != nil {
return nil, err return nil, err

View File

@@ -14,12 +14,12 @@ import (
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
) )
func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) { func TestInitiateByQuote_ForwardsClientPaymentRef(t *testing.T) {
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec) api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}` body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","clientPaymentRef":"client-ref-1"}`
rr := invokeInitiateByQuote(t, api, orgRef, body) rr := invokeInitiateByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want { if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String()) t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -32,6 +32,24 @@ func TestInitiateByQuote_DoesNotUseIntentRef(t *testing.T) {
} }
} }
func TestInitiateByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-by-quote","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}`
rr := invokeInitiateByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeReqs), 1; got != want {
t.Fatalf("execute calls mismatch: got=%d want=%d", got, want)
}
if got := exec.executeReqs[0].GetClientPaymentRef(); got != "" {
t.Fatalf("expected empty client_payment_ref, got=%q", got)
}
}
func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) { func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}
@@ -50,7 +68,7 @@ func TestInitiateByQuote_RejectsMetadataIntentRef(t *testing.T) {
func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder { func invokeInitiateByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper() t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-quote", bytes.NewBufferString(body)) req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-quote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext() routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex()) routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
@@ -39,7 +40,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
return response.BadPayload(a.logger, a.Name(), err) return response.BadPayload(a.logger, a.Name(), err)
} }
clientPaymentRef := metadataValue(payload.Metadata, "client_payment_ref") clientPaymentRef := strings.TrimSpace(payload.ClientPaymentRef)
idempotencyKey := strings.TrimSpace(payload.IdempotencyKey) idempotencyKey := strings.TrimSpace(payload.IdempotencyKey)
quotationRef := strings.TrimSpace(payload.QuoteRef) quotationRef := strings.TrimSpace(payload.QuoteRef)
@@ -50,7 +51,7 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
} }
resp, err := a.execution.ExecuteBatchPayment(ctx, req) resp, err := a.execution.ExecuteBatchPayment(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to initiate batch payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to initiate batch payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return grpcErrorResponse(a.logger, a.Name(), err) return grpcErrorResponse(a.logger, a.Name(), err)
} }
@@ -62,7 +63,9 @@ func (a *PaymentAPI) initiatePaymentsByQuote(r *http.Request, account *model.Acc
} }
func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) { func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments, error) {
defer r.Body.Close() defer func() {
_ = r.Body.Close()
}()
payload := &srequest.InitiatePayments{} payload := &srequest.InitiatePayments{}
decoder := json.NewDecoder(r.Body) decoder := json.NewDecoder(r.Body)
@@ -72,6 +75,7 @@ func decodeInitiatePaymentsPayload(r *http.Request) (*srequest.InitiatePayments,
} }
payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey) payload.IdempotencyKey = strings.TrimSpace(payload.IdempotencyKey)
payload.QuoteRef = strings.TrimSpace(payload.QuoteRef) payload.QuoteRef = strings.TrimSpace(payload.QuoteRef)
payload.ClientPaymentRef = strings.TrimSpace(payload.ClientPaymentRef)
if err := payload.Validate(); err != nil { if err := payload.Validate(); err != nil {
return nil, err return nil, err

View File

@@ -10,7 +10,6 @@ import (
"github.com/go-chi/chi/v5" "github.com/go-chi/chi/v5"
"github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/auth"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2" orchestrationv2 "github.com/tech/sendico/pkg/proto/payments/orchestration/v2"
@@ -50,7 +49,7 @@ func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec) api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"client-ref-1"}}` body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","clientPaymentRef":"client-ref-1"}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body) rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want { if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String()) t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
@@ -67,6 +66,25 @@ func TestInitiatePaymentsByQuote_ForwardsClientPaymentRef(t *testing.T) {
} }
} }
func TestInitiatePaymentsByQuote_DoesNotForwardLegacyClientPaymentRefFromMetadata(t *testing.T) {
orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{}
api := newBatchAPI(exec)
body := `{"idempotencyKey":"idem-batch","quoteRef":"quote-1","metadata":{"client_payment_ref":"legacy-client-ref"}}`
rr := invokeInitiatePaymentsByQuote(t, api, orgRef, body)
if got, want := rr.Code, http.StatusOK; got != want {
t.Fatalf("status mismatch: got=%d want=%d body=%s", got, want, rr.Body.String())
}
if got, want := len(exec.executeBatchReqs), 1; got != want {
t.Fatalf("execute batch calls mismatch: got=%d want=%d", got, want)
}
if got := exec.executeBatchReqs[0].GetClientPaymentRef(); got != "" {
t.Fatalf("expected empty client_payment_ref, got=%q", got)
}
}
func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) { func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefField(t *testing.T) {
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
exec := &fakeExecutionClientForBatch{} exec := &fakeExecutionClientForBatch{}
@@ -99,7 +117,7 @@ func TestInitiatePaymentsByQuote_RejectsDeprecatedIntentRefsField(t *testing.T)
func newBatchAPI(exec executionClient) *PaymentAPI { func newBatchAPI(exec executionClient) *PaymentAPI {
return &PaymentAPI{ return &PaymentAPI{
logger: mlogger.Logger(zap.NewNop()), logger: zap.NewNop(),
execution: exec, execution: exec,
enf: fakeEnforcerForBatch{allowed: true}, enf: fakeEnforcerForBatch{allowed: true},
oph: mutil.CreatePH(mservice.Organizations), oph: mutil.CreatePH(mservice.Organizations),
@@ -110,7 +128,7 @@ func newBatchAPI(exec executionClient) *PaymentAPI {
func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder { func invokeInitiatePaymentsByQuote(t *testing.T, api *PaymentAPI, orgRef bson.ObjectID, body string) *httptest.ResponseRecorder {
t.Helper() t.Helper()
req := httptest.NewRequest(http.MethodPost, "/by-multiquote", bytes.NewBufferString(body)) req := httptest.NewRequestWithContext(context.Background(), http.MethodPost, "/by-multiquote", bytes.NewBufferString(body))
routeCtx := chi.NewRouteContext() routeCtx := chi.NewRouteContext()
routeCtx.URLParams.Add("organizations_ref", orgRef.Hex()) routeCtx.URLParams.Add("organizations_ref", orgRef.Hex())
req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx)) req = req.WithContext(context.WithValue(req.Context(), chi.RouteCtxKey, routeCtx))
@@ -147,6 +165,10 @@ func (*fakeExecutionClientForBatch) ListPayments(context.Context, *orchestration
return &orchestrationv2.ListPaymentsResponse{}, nil return &orchestrationv2.ListPaymentsResponse{}, nil
} }
func (*fakeExecutionClientForBatch) GetPayment(context.Context, *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error) {
return &orchestrationv2.GetPaymentResponse{}, nil
}
func (*fakeExecutionClientForBatch) Close() error { return nil } func (*fakeExecutionClientForBatch) Close() error { return nil }
type fakeEnforcerForBatch struct { type fakeEnforcerForBatch struct {
@@ -158,6 +180,7 @@ func (f fakeEnforcerForBatch) Enforce(context.Context, bson.ObjectID, bson.Objec
} }
func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) { func (fakeEnforcerForBatch) EnforceBatch(context.Context, []model.PermissionBoundStorable, bson.ObjectID, model.Action) (map[bson.ObjectID]bool, error) {
//nolint:nilnil // Test stub does not provide batch permissions map.
return nil, nil return nil, nil
} }

View File

@@ -8,6 +8,7 @@ import (
"github.com/tech/sendico/pkg/api/http/response" "github.com/tech/sendico/pkg/api/http/response"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap"
quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2" quotationv2 "github.com/tech/sendico/pkg/proto/payments/quotation/v2"
"github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/srequest"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
@@ -61,7 +62,7 @@ func (a *PaymentAPI) quotePayment(r *http.Request, account *model.Account, token
resp, err := a.quotation.QuotePayment(ctx, req) resp, err := a.quotation.QuotePayment(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to quote payment", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to quote payment", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return grpcErrorResponse(a.logger, a.Name(), err) return grpcErrorResponse(a.logger, a.Name(), err)
} }
@@ -117,7 +118,7 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
resp, err := a.quotation.QuotePayments(ctx, req) resp, err := a.quotation.QuotePayments(ctx, req)
if err != nil { if err != nil {
a.logger.Warn("Failed to quote payments", zap.Error(err), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to quote payments", zap.Error(err), mzap.ObjRef("organization_ref", orgRef))
return grpcErrorResponse(a.logger, a.Name(), err) return grpcErrorResponse(a.logger, a.Name(), err)
} }
@@ -125,7 +126,9 @@ func (a *PaymentAPI) quotePayments(r *http.Request, account *model.Account, toke
} }
func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) { func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
defer r.Body.Close() defer func() {
_ = r.Body.Close()
}()
payload := &srequest.QuotePayment{} payload := &srequest.QuotePayment{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil { if err := json.NewDecoder(r.Body).Decode(payload); err != nil {
@@ -139,7 +142,9 @@ func decodeQuotePayload(r *http.Request) (*srequest.QuotePayment, error) {
} }
func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) { func decodeQuotePaymentsPayload(r *http.Request) (*srequest.QuotePayments, error) {
defer r.Body.Close() defer func() {
_ = r.Body.Close()
}()
payload := &srequest.QuotePayments{} payload := &srequest.QuotePayments{}
if err := json.NewDecoder(r.Body).Decode(payload); err != nil { if err := json.NewDecoder(r.Body).Decode(payload); err != nil {

View File

@@ -32,6 +32,7 @@ import (
type executionClient interface { type executionClient interface {
ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error) ExecutePayment(ctx context.Context, req *orchestrationv2.ExecutePaymentRequest) (*orchestrationv2.ExecutePaymentResponse, error)
ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error) ExecuteBatchPayment(ctx context.Context, req *orchestrationv2.ExecuteBatchPaymentRequest) (*orchestrationv2.ExecuteBatchPaymentResponse, error)
GetPayment(ctx context.Context, req *orchestrationv2.GetPaymentRequest) (*orchestrationv2.GetPaymentResponse, error)
ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error) ListPayments(ctx context.Context, req *orchestrationv2.ListPaymentsRequest) (*orchestrationv2.ListPaymentsResponse, error)
Close() error Close() error
} }
@@ -106,7 +107,7 @@ func CreateAPI(apiCtx eapi.API) (*PaymentAPI, error) {
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote) apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-quote"), api.Post, p.initiateByQuote)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote) apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/by-multiquote"), api.Post, p.initiatePaymentsByQuote)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listPayments) apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/"), api.Get, p.listPayments)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/documents/act"), api.Get, p.getActDocument) apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/documents/operation"), api.Get, p.getOperationDocument)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/registry"), api.Get, p.listDiscoveryRegistry) apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/registry"), api.Get, p.listDiscoveryRegistry)
apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/registry/refresh"), api.Get, p.getDiscoveryRefresh) apiCtx.Register().AccountHandler(p.Name(), p.oph.AddRef("/registry/refresh"), api.Get, p.getDiscoveryRefresh)
@@ -207,15 +208,12 @@ type grpcQuotationClient struct {
callTimeout time.Duration callTimeout time.Duration
} }
func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) { func newQuotationClient(_ context.Context, cfg quotationClientConfig, opts ...grpc.DialOption) (quotationClient, error) {
cfg.setDefaults() cfg.setDefaults()
if strings.TrimSpace(cfg.Address) == "" { if strings.TrimSpace(cfg.Address) == "" {
return nil, merrors.InvalidArgument("payment quotation: address is required") return nil, merrors.InvalidArgument("payment quotation: address is required")
} }
dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout)
defer cancel()
dialOpts := make([]grpc.DialOption, 0, len(opts)+1) dialOpts := make([]grpc.DialOption, 0, len(opts)+1)
dialOpts = append(dialOpts, opts...) dialOpts = append(dialOpts, opts...)
if cfg.Insecure { if cfg.Insecure {
@@ -224,7 +222,7 @@ func newQuotationClient(ctx context.Context, cfg quotationClientConfig, opts ...
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
} }
conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) conn, err := grpc.NewClient(cfg.Address, dialOpts...)
if err != nil { if err != nil {
return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-quotation: dial %s", cfg.Address)) return nil, merrors.InternalWrap(err, fmt.Sprintf("payment-quotation: dial %s", cfg.Address))
} }
@@ -259,6 +257,7 @@ func (c *grpcQuotationClient) callContext(ctx context.Context) (context.Context,
if timeout <= 0 { if timeout <= 0 {
timeout = 3 * time.Second timeout = 3 * time.Second
} }
//nolint:gosec // Caller receives cancel func and defers it in every call path.
return context.WithTimeout(ctx, timeout) return context.WithTimeout(ctx, timeout)
} }
@@ -274,7 +273,7 @@ func (a *PaymentAPI) initDiscoveryClient(cfg *eapi.Config) error {
if err != nil { if err != nil {
return err return err
} }
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name())) client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
if err != nil { if err != nil {
return err return err
} }

View File

@@ -90,5 +90,5 @@ func (a *PermissionsAPI) changePoliciesImp(
} }
} }
return nil, nil return struct{}{}, nil
} }

View File

@@ -12,6 +12,7 @@ import (
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mservice" "github.com/tech/sendico/pkg/mservice"
"github.com/tech/sendico/pkg/mutil/mzap"
connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1"
"github.com/tech/sendico/server/interface/api/sresponse" "github.com/tech/sendico/server/interface/api/sresponse"
mutil "github.com/tech/sendico/server/internal/mutil/param" mutil "github.com/tech/sendico/server/internal/mutil/param"
@@ -65,24 +66,24 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
return response.Auto(a.logger, a.Name(), merrors.NoData("no crypto gateways available")) return response.Auto(a.logger, a.Name(), merrors.NoData("no crypto gateways available"))
} }
a.logger.Debug("Resolved CRYPTO gateways for wallet balance lookup", a.logger.Debug("Resolved CRYPTO gateways for wallet balance lookup",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
route, routeErr := a.walletRoute(ctx, orgRef.Hex(), walletRef) route, routeErr := a.walletRoute(ctx, orgRef.Hex(), walletRef)
if routeErr != nil { if routeErr != nil {
a.logger.Warn("Failed to resolve wallet route", zap.Error(routeErr), zap.String("wallet_ref", walletRef), zap.String("organization_ref", orgRef.Hex())) a.logger.Warn("Failed to resolve wallet route", zap.Error(routeErr), zap.String("wallet_ref", walletRef), mzap.ObjRef("organization_ref", orgRef))
} }
if route != nil { if route != nil {
a.logger.Debug("Resolved stored wallet route", a.logger.Debug("Resolved stored wallet route",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("route_network", route.Network), zap.String("route_network", route.Network),
zap.String("route_gateway_id", route.GatewayID)) zap.String("route_gateway_id", route.GatewayID))
preferred := findGatewayForRoute(cryptoGateways, route) preferred := findGatewayForRoute(cryptoGateways, route)
if preferred != nil { if preferred != nil {
a.logger.Debug("Using preferred gateway from stored wallet route", a.logger.Debug("Using preferred gateway from stored wallet route",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("gateway_id", preferred.ID), zap.String("gateway_id", preferred.ID),
zap.String("network", preferred.Network), zap.String("network", preferred.Network),
@@ -91,7 +92,7 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
if preferredErr == nil && bal != nil { if preferredErr == nil && bal != nil {
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, preferred.Network, preferred.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, preferred.Network, preferred.ID)
a.logger.Debug("Wallet balance resolved via preferred gateway", a.logger.Debug("Wallet balance resolved via preferred gateway",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("gateway_id", preferred.ID), zap.String("gateway_id", preferred.ID),
zap.String("network", preferred.Network)) zap.String("network", preferred.Network))
@@ -124,20 +125,20 @@ func (a *WalletAPI) getWalletBalance(r *http.Request, account *model.Account, to
} }
} else { } else {
a.logger.Warn("Stored wallet route did not match any healthy discovery gateway", a.logger.Warn("Stored wallet route did not match any healthy discovery gateway",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("route_network", route.Network), zap.String("route_network", route.Network),
zap.String("route_gateway_id", route.GatewayID)) zap.String("route_gateway_id", route.GatewayID))
} }
} else { } else {
a.logger.Debug("Stored wallet route not found; using gateway fallback", a.logger.Debug("Stored wallet route not found; using gateway fallback",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef)) zap.String("wallet_ref", walletRef))
} }
// Fall back to querying remaining gateways in parallel. // Fall back to querying remaining gateways in parallel.
a.logger.Debug("Starting fallback wallet balance fan-out", a.logger.Debug("Starting fallback wallet balance fan-out",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
bal, err := a.queryBalanceFromGateways(ctx, cryptoGateways, orgRef.Hex(), walletRef) bal, err := a.queryBalanceFromGateways(ctx, cryptoGateways, orgRef.Hex(), walletRef)
@@ -222,14 +223,11 @@ func (a *WalletAPI) queryBalanceFromGateways(ctx context.Context, gateways []dis
a.logger.Debug("Wallet balance fan-out completed without result", a.logger.Debug("Wallet balance fan-out completed without result",
zap.String("organization_ref", organizationRef), zap.String("organization_ref", organizationRef),
zap.String("wallet_ref", walletRef)) zap.String("wallet_ref", walletRef))
//nolint:nilnil // No gateway returned a balance and no hard error occurred.
return nil, nil return nil, nil
} }
func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.GatewaySummary, walletRef string) (*connectorv1.Balance, error) { func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.GatewaySummary, walletRef string) (*connectorv1.Balance, error) {
// Create connection with timeout
dialCtx, cancel := context.WithTimeout(ctx, a.dialTimeout)
defer cancel()
var dialOpts []grpc.DialOption var dialOpts []grpc.DialOption
if a.insecure { if a.insecure {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
@@ -237,11 +235,15 @@ func (a *WalletAPI) queryGatewayBalance(ctx context.Context, gateway discovery.G
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
} }
conn, err := grpc.DialContext(dialCtx, gateway.InvokeURI, dialOpts...) conn, err := grpc.NewClient(gateway.InvokeURI, dialOpts...)
if err != nil { if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway") return nil, merrors.InternalWrap(err, "dial gateway")
} }
defer conn.Close() defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn) client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -81,7 +81,7 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("no gateway available for network: "+networkName)) return response.Auto(a.logger, a.Name(), merrors.InvalidArgument("no gateway available for network: "+networkName))
} }
a.logger.Debug("Selected gateway for wallet creation", a.logger.Debug("Selected gateway for wallet creation",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("network", networkName), zap.String("network", networkName),
zap.String("gateway_id", gateway.ID), zap.String("gateway_id", gateway.ID),
zap.String("gateway_network", gateway.Network), zap.String("gateway_network", gateway.Network),
@@ -134,7 +134,7 @@ func (a *WalletAPI) create(r *http.Request, account *model.Account, token *sresp
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, networkName, gateway.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, networkName, gateway.ID)
a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, gateway.Network, gateway.ID) a.rememberWalletRoute(ctx, orgRef.Hex(), walletRef, gateway.Network, gateway.ID)
a.logger.Debug("Persisted wallet route after wallet creation", a.logger.Debug("Persisted wallet route after wallet creation",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.String("wallet_ref", walletRef), zap.String("wallet_ref", walletRef),
zap.String("network", networkName), zap.String("network", networkName),
zap.String("gateway_id", gateway.ID)) zap.String("gateway_id", gateway.ID))
@@ -162,10 +162,6 @@ func findGatewayForNetwork(gateways []discovery.GatewaySummary, network string)
} }
func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery.GatewaySummary, req *connectorv1.OpenAccountRequest) (string, error) { func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery.GatewaySummary, req *connectorv1.OpenAccountRequest) (string, error) {
// Create connection with timeout
dialCtx, cancel := context.WithTimeout(ctx, a.dialTimeout)
defer cancel()
var dialOpts []grpc.DialOption var dialOpts []grpc.DialOption
if a.insecure { if a.insecure {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
@@ -173,11 +169,15 @@ func (a *WalletAPI) createWalletOnGateway(ctx context.Context, gateway discovery
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
} }
conn, err := grpc.DialContext(dialCtx, gateway.InvokeURI, dialOpts...) conn, err := grpc.NewClient(gateway.InvokeURI, dialOpts...)
if err != nil { if err != nil {
return "", merrors.InternalWrap(err, "dial gateway") return "", merrors.InternalWrap(err, "dial gateway")
} }
defer conn.Close() defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn) client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -59,7 +59,7 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
return sresponse.Wallets(a.logger, nil, token) return sresponse.Wallets(a.logger, nil, token)
} }
a.logger.Debug("Resolved CRYPTO gateways for wallet list", a.logger.Debug("Resolved CRYPTO gateways for wallet list",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
// Build request // Build request
@@ -80,7 +80,7 @@ func (a *WalletAPI) listWallets(r *http.Request, account *model.Account, token *
allAccounts := a.queryAllGateways(ctx, cryptoGateways, req) allAccounts := a.queryAllGateways(ctx, cryptoGateways, req)
dedupedAccounts := dedupeAccountsByWalletRef(allAccounts) dedupedAccounts := dedupeAccountsByWalletRef(allAccounts)
a.logger.Debug("Wallet list fan-out completed", a.logger.Debug("Wallet list fan-out completed",
zap.String("organization_ref", orgRef.Hex()), mzap.ObjRef("organization_ref", orgRef),
zap.Int("accounts_raw", len(allAccounts)), zap.Int("accounts_raw", len(allAccounts)),
zap.Int("accounts_deduped", len(dedupedAccounts)), zap.Int("accounts_deduped", len(dedupedAccounts)),
zap.Int("gateway_count", len(cryptoGateways))) zap.Int("gateway_count", len(cryptoGateways)))
@@ -215,10 +215,6 @@ func (a *WalletAPI) queryAllGateways(ctx context.Context, gateways []discovery.G
} }
func (a *WalletAPI) queryGateway(ctx context.Context, gateway discovery.GatewaySummary, req *connectorv1.ListAccountsRequest) ([]*connectorv1.Account, error) { func (a *WalletAPI) queryGateway(ctx context.Context, gateway discovery.GatewaySummary, req *connectorv1.ListAccountsRequest) ([]*connectorv1.Account, error) {
// Create connection with timeout
dialCtx, cancel := context.WithTimeout(ctx, a.dialTimeout)
defer cancel()
var dialOpts []grpc.DialOption var dialOpts []grpc.DialOption
if a.insecure { if a.insecure {
dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials()))
@@ -226,11 +222,15 @@ func (a *WalletAPI) queryGateway(ctx context.Context, gateway discovery.GatewayS
dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{})))
} }
conn, err := grpc.DialContext(dialCtx, gateway.InvokeURI, dialOpts...) conn, err := grpc.NewClient(gateway.InvokeURI, dialOpts...)
if err != nil { if err != nil {
return nil, merrors.InternalWrap(err, "dial gateway") return nil, merrors.InternalWrap(err, "dial gateway")
} }
defer conn.Close() defer func() {
if closeErr := conn.Close(); closeErr != nil {
a.logger.Warn("Failed to close gateway connection", zap.Error(closeErr), zap.String("gateway", gateway.ID))
}
}()
client := connectorv1.NewConnectorServiceClient(conn) client := connectorv1.NewConnectorServiceClient(conn)

View File

@@ -64,18 +64,21 @@ func (a *WalletAPI) rememberWalletRoute(ctx context.Context, organizationRef str
func (a *WalletAPI) walletRoute(ctx context.Context, organizationRef string, walletRef string) (*model.ChainWalletRoute, error) { func (a *WalletAPI) walletRoute(ctx context.Context, organizationRef string, walletRef string) (*model.ChainWalletRoute, error) {
if a.routes == nil { if a.routes == nil {
//nolint:nilnil // Routing cache is optional and may be disabled.
return nil, nil return nil, nil
} }
walletRef = strings.TrimSpace(walletRef) walletRef = strings.TrimSpace(walletRef)
organizationRef = strings.TrimSpace(organizationRef) organizationRef = strings.TrimSpace(organizationRef)
if walletRef == "" || organizationRef == "" { if walletRef == "" || organizationRef == "" {
//nolint:nilnil // Missing route keys mean no cached route.
return nil, nil return nil, nil
} }
route, err := a.routes.Get(ctx, organizationRef, walletRef) route, err := a.routes.Get(ctx, organizationRef, walletRef)
if err != nil { if err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
//nolint:nilnil // Route not found in cache.
return nil, nil return nil, nil
} }
return nil, err return nil, err

View File

@@ -129,7 +129,7 @@ func (a *WalletAPI) initDiscoveryClient(cfg *eapi.Config) error {
if err != nil { if err != nil {
return err return err
} }
client, err := discovery.NewClient(a.logger, broker, nil, string(a.Name())) client, err := discovery.NewClient(a.logger, broker, nil, a.Name())
if err != nil { if err != nil {
return err return err
} }

View File

@@ -9,8 +9,8 @@ import (
) )
// generate translations // generate translations
// go:generate Users/stephandeshevikh/go/bin/go18n extract //go:generate Users/stephandeshevikh/go/bin/go18n extract
// go:generate Users/stephandeshevikh/go/bin/go18n merge //go:generate Users/stephandeshevikh/go/bin/go18n merge
// lint go code // lint go code
// docker run -t --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 10m0s --enable-all -D ireturn -D wrapcheck -D varnamelen -D tagliatelle -D nosnakecase -D gochecknoglobals -D nlreturn -D stylecheck -D lll -D wsl -D scopelint -D varcheck -D exhaustivestruct -D golint -D maligned -D interfacer -D ifshort -D structcheck -D deadcode -D godot -D depguard -D tagalign // docker run -t --rm -v $(pwd):/app -w /app golangci/golangci-lint:latest golangci-lint run -v --timeout 10m0s --enable-all -D ireturn -D wrapcheck -D varnamelen -D tagliatelle -D nosnakecase -D gochecknoglobals -D nlreturn -D stylecheck -D lll -D wsl -D scopelint -D varcheck -D exhaustivestruct -D golint -D maligned -D interfacer -D ifshort -D structcheck -D deadcode -D godot -D depguard -D tagalign

View File

@@ -0,0 +1,47 @@
version: "2"
linters:
default: none
enable:
- bodyclose
- canonicalheader
- copyloopvar
- durationcheck
- errcheck
- errchkjson
- errname
- errorlint
- gosec
- govet
- ineffassign
- nilerr
- nilnesserr
- nilnil
- noctx
- rowserrcheck
- sqlclosecheck
- staticcheck
- unconvert
- wastedassign
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gochecknoinits
- gomoddirectives
- wrapcheck
- cyclop
- dupl
- funlen
- gocognit
- gocyclo
- ireturn
- lll
- mnd
- nestif
- nlreturn
- noinlineerr
- paralleltest
- tagliatelle
- testpackage
- varnamelen
- wsl_v5

Some files were not shown because too many files have changed in this diff Show More