329 Commits

Author SHA1 Message Date
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
ddd5e36275 Merge pull request 'added service reannounce' (#594) from discovery-593 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/billing_documents Pipeline failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/callbacks Pipeline failed
ci/woodpecker/push/discovery Pipeline failed
ci/woodpecker/push/fx_ingestor Pipeline failed
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline failed
ci/woodpecker/push/gateway_chain Pipeline failed
Reviewed-on: #594
2026-03-01 13:02:46 +00:00
Stephan D
ce23de94ce added service reannounce 2026-03-01 14:02:05 +01:00
1005201bb7 Merge pull request 'fixed client id' (#592) from bff-591 into main
Some checks failed
ci/woodpecker/push/discovery Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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 was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/callbacks Pipeline failed
Reviewed-on: #592
2026-03-01 12:40:31 +00:00
Stephan D
38077c1ed8 fixed client id 2026-03-01 13:40:02 +01:00
d0368f5a00 Merge pull request 'bff for callbacks' (#590) from bff-589 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/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend 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_oracle 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
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
Reviewed-on: #590
2026-03-01 01:04:50 +00:00
Stephan D
86eab3bb70 bff for callbacks 2026-03-01 02:04:15 +01:00
709df51512 Merge pull request 'cb-586' (#588) from cb-586 into main
All checks were successful
ci/woodpecker/push/callbacks Pipeline was successful
Reviewed-on: #588
2026-02-28 23:28:18 +00:00
Stephan D
f914575a65 fixed entrypoint command 2026-03-01 00:08:05 +01:00
Stephan D
a6d560eb10 fixed entrypoint command 2026-02-28 23:22:53 +01:00
3cbe07a1ec Merge pull request 'cb-586' (#587) from cb-586 into main
Some checks failed
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 failed
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/ledger Pipeline failed
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_quotation Pipeline failed
ci/woodpecker/push/payments_orchestrator 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
Reviewed-on: #587
2026-02-28 20:02:08 +00:00
Stephan D
598510f487 versions bump 2026-02-28 21:01:39 +01:00
Stephan D
12c67361dd refactored orchestrator and callbacks service to use pkg messsaging + envelope factory / handler 2026-02-28 20:56:26 +01:00
363d6474f2 Merge pull request 'readme update' (#584) from cb-582 into main
Reviewed-on: #584
2026-02-28 19:11:28 +00:00
Stephan D
004355f7d5 readme update 2026-02-28 20:06:41 +01:00
1f67fba3e4 Merge pull request 'cb-582' (#583) from cb-582 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 failed
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/fx_ingestor 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: #583
2026-02-28 09:17:37 +00:00
Stephan D
4c3132bbc1 versions bump 2026-02-28 10:12:54 +01:00
Stephan D
0f28f2d088 callbacks service draft 2026-02-28 10:10:26 +01:00
b7900d3beb Merge pull request 'api login method' (#581) from bff-580 into main
Some checks failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #581
2026-02-28 09:08:21 +00:00
Stephan D
800f8c12f8 api login method 2026-02-28 10:07:52 +01:00
f50313c30b Merge pull request 'move api/server to api/edge/bff' (#579) from bff-578 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/discovery 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_mntx 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/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: #579
2026-02-27 23:39:42 +00:00
Stephan D
98db0e4e9e move api/server to api/edge/bff 2026-02-28 00:39:20 +01:00
34182af3b8 Merge pull request 'discovery stale records dropout implemented' (#577) from discovery-576 into main
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff 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/frontend 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: #577
2026-02-27 17:08:06 +00:00
Stephan D
d47159278b discovery stale records dropout implemented 2026-02-27 18:07:42 +01:00
077c510690 Merge pull request 'got rid of fees dependency in ledger' (#575) from ledger-565 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/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_mntx 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/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: #575
2026-02-27 15:24:38 +00:00
Stephan D
605f0ba139 got rid of fees dependency in ledger 2026-02-27 16:23:50 +01:00
02a0d192b9 Merge pull request 'year normalization' (#574) from mntx-573 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #574
2026-02-27 15:16:26 +00:00
Stephan D
128e5392e1 year normalization 2026-02-27 16:15:46 +01:00
c462b8fa69 Merge pull request 'fixed contract address handling' (#572) from tron-570 into main
Some checks failed
ci/woodpecker/push/gateway_tron Pipeline failed
ci/woodpecker/push/gateway_chain Pipeline was successful
Reviewed-on: #572
2026-02-27 13:29:28 +00:00
Stephan D
bec969cf8b fixed contract address handling 2026-02-27 14:24:43 +01:00
92253de6f3 Merge pull request 'fixed payment reference' (#571) from tg-567 into main
All checks were successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #571
2026-02-27 13:22:18 +00:00
Stephan D
6fe6b7a932 fixed payment reference 2026-02-27 14:15:55 +01:00
fea71779b9 Merge pull request 'fixed test' (#568) from tg-567 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #568
2026-02-27 13:13:21 +00:00
Stephan D
d1ebd4b009 fixed test 2026-02-27 14:11:30 +01:00
Stephan D
651f103abd fixed message status update
Some checks failed
ci/woodpecker/push/gateway_tgsettle Pipeline failed
2026-02-27 13:51:08 +01:00
57428c5c56 Merge pull request 'fixed processing status' (#566) from fr-568 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #566
2026-02-27 12:10:51 +00:00
Stephan D
5113568a1b fixed processing status 2026-02-27 13:08:23 +01:00
5f4e580b2c Merge pull request 'gateway limits fix' (#564) from mntx-563 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #564
2026-02-27 02:58:43 +00:00
Stephan D
039a41e6d8 gateway limits fix 2026-02-27 03:57:52 +01:00
98770eda1e Merge pull request '+autoremoval of stale discovery records' (#562) from discovery-566 into main
Some checks failed
ci/woodpecker/push/billing_fees 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 was successful
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/gateway_chain Pipeline failed
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline failed
ci/woodpecker/push/ledger Pipeline failed
ci/woodpecker/push/gateway_tron Pipeline failed
Reviewed-on: #562
2026-02-27 01:55:18 +00:00
Stephan D
fc5d44364b +autoremoval of stale discovery records 2026-02-27 02:53:55 +01:00
55ddfff4f2 Merge pull request 'fixed rail & operation names' (#560) from rail-559 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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 was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline failed
ci/woodpecker/push/billing_fees Pipeline failed
Reviewed-on: #560
2026-02-27 01:34:15 +00:00
Stephan D
747153bdbf fixed rail & operation names 2026-02-27 02:33:40 +01:00
82cf91e703 Merge pull request 'fixed mntx rail' (#558) from mntx-557 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #558
2026-02-27 00:45:54 +00:00
Stephan D
9fd9cba92a fixed mntx rail 2026-02-27 01:45:29 +01:00
4ea39c44ef Merge pull request 'docs fix' (#556) from docs-555 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #556
2026-02-27 00:30:22 +00:00
Stephan D
7d6d7c3f56 docs fix 2026-02-27 01:29:57 +01:00
560e2213b5 [infra][rebuild]Merge pull request 'Orchestration / payments v2' (#554) from pqpov2-547 into main
All checks were successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db 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_mntx 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/nats 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
[infra][rebuild]Reviewed-on: #554
2026-02-26 22:45:54 +00:00
Stephan D
a794aff5f3 release version bump 2026-02-26 23:37:09 +01:00
Stephan D
fa9e6f47cf smarter optimizer for batch payments 2026-02-26 23:15:48 +01:00
Stephan D
947cd7f4c9 dto / mapper / model separation of verification purpose 2026-02-26 22:38:31 +01:00
Stephan D
20ce4485e8 missing proto definition 2026-02-26 22:29:18 +01:00
Stephan D
e8d763eb15 removed intent_ref from frontend 2026-02-26 22:20:54 +01:00
Stephan D
4949c4ffe0 Batch payment execution + got rid of intent references 2026-02-26 22:12:32 +01:00
Stephan D
7661038868 intent reference generation + propagation 2026-02-26 18:43:44 +01:00
Stephan D
0f95f898a8 gRPC error translation: invalid argument support 2026-02-26 16:59:09 +01:00
Stephan D
b4b5616de0 removed dead mntx dependency + dead settings 2026-02-26 16:32:10 +01:00
Stephan D
336f352858 Fixed billing fees unreachable error propagation. Added USDT ledger creation. Fixed ledger boundaries operation types 2026-02-26 16:25:52 +01:00
Stephan D
54e5c799e8 fixed ledger boundaries operatoin types 2026-02-26 10:18:40 +01:00
Stephan D
70b1c2a9cc +source currency pick fix +fx side propagation 2026-02-26 02:39:48 +01:00
Stephan D
008427483c - legacy payment template fee lines picking 2026-02-25 23:20:03 +01:00
Stephan D
7235ca1897 restricted CORS settings 2026-02-25 23:02:18 +01:00
Stephan D
53abb24482 +ledger ops 2026-02-25 22:17:12 +01:00
84cd23d5a1 Merge pull request 'fixed caddyfile' (#552) from SEND059 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/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_mntx 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/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: #552
2026-02-25 20:59:00 +00:00
Arseni
d05a5430f1 final fix 2026-02-25 23:58:02 +03:00
Arseni
70359916b9 fixed caddyfile 2026-02-25 23:49:18 +03:00
ebde599c38 Merge pull request 'woodpecker addition' (#551) from SEND058 into main
All checks were 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/billing_fees 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_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle 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: #551
2026-02-25 19:11:18 +00:00
Stephan D
26bedc5743 +implementation of settlement step execution 2026-02-25 20:09:45 +01:00
Arseni
38b347f02e woodpecker addition 2026-02-25 22:09:23 +03:00
Stephan D
af4b68f4c7 Improved payment handling 2026-02-25 19:25:51 +01:00
6c98dc2c0c Merge pull request 'small woodpecker additioin for build to run' (#549) from SEND057 into main
Reviewed-on: #549
2026-02-25 11:36:36 +00:00
Arseni
dd61fe155f small woodpecker additioin for build to run 2026-02-25 12:20:22 +03:00
11c37d286c Merge pull request 'added api docs' (#548) from SEND056 into main
Reviewed-on: #548
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-25 00:39:11 +00:00
Arseni
d70d9e84c9 small fixes 2026-02-25 01:01:28 +03:00
Stephan D
da11be526a removed legacy from bff 2026-02-24 21:18:23 +01:00
Arseni
fa54088b25 added api docs 2026-02-24 21:26:31 +03:00
Stephan D
a998b59072 complete MECE request 2026-02-24 18:33:12 +01:00
Stephan D
4c5677202a mece request / payment economics 2026-02-24 18:02:20 +01:00
Stephan D
2e08ec9b9b fee treatment added 2026-02-24 16:39:08 +01:00
Stephan D
2fe90347a8 quotation service fixed 2026-02-24 16:14:09 +01:00
Stephan D
6444813f38 payment quotation v2 + payment orchestration v2 draft 2026-02-24 13:01:35 +01:00
0646f55189 Merge pull request 'redesigned payment page + a lot of fix' (#546) from SEND055 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #546
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-23 12:41:44 +00:00
Arseni
0c6fa03aba redesigned payment page + a lot of fixes 2026-02-21 21:55:20 +03:00
a68aa2abff Merge pull request 'set 10 min quotations timeout' (#545) from quote-544 into main
All checks were successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #545
2026-02-20 16:20:11 +00:00
Stephan D
51514159f5 set 10 min quotations timeout 2026-02-20 17:19:31 +01:00
199811ba09 Merge pull request 'cached gateway routing' (#543) from gw-542 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/discovery 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_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: #543
2026-02-20 14:38:46 +00:00
Stephan D
671ccc55a0 cached gateway routing 2026-02-20 15:38:22 +01:00
bc2bc3770d Merge pull request 'wallets listing dedupe' (#541) from wallet-540 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/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_methods 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/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #541
2026-02-20 12:52:45 +00:00
Stephan D
20cb057618 wallets listing dedupe 2026-02-20 13:52:09 +01:00
e23484ddff Merge pull request 'missing error handling' (#539) from tg-537 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #539
2026-02-19 20:35:27 +00:00
Stephan D
4344ae89e0 missing error handling 2026-02-19 21:35:04 +01:00
352a4a524b Merge pull request 'extended confirmation flow handling' (#538) from tg-537 into main
All checks were 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
Reviewed-on: #538
2026-02-19 20:08:41 +00:00
Stephan D
2bf05ab414 extended confirmation flow handling 2026-02-19 21:08:24 +01:00
14eb99e221 Merge pull request 'migrated raw mongo.Collection to repository.Repository + chain driver resolution fix' (#536) from tg-535 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
Reviewed-on: #536
2026-02-19 19:31:08 +00:00
Stephan D
afd8d8d01e migrated raw mongo.Collection to repository.Repository + chain driver resolution fix 2026-02-19 20:22:41 +01:00
b6cced6947 Merge pull request 'refactored notificatoin / tgsettle responsibility boundaries' (#534) from tg-530 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/discovery 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_mntx 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/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: #534
2026-02-19 17:57:47 +00:00
Stephan D
2fd8a6ebb7 refactored notificatoin / tgsettle responsibility boundaries 2026-02-19 18:56:59 +01:00
6d21f08e10 Merge pull request 'extended logging' (#532) from tg-530 into main
All checks were successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #532
2026-02-19 17:06:35 +00:00
Stephan D
47f0a3d890 extended logging 2026-02-19 18:06:14 +01:00
9f3ba8f610 Merge pull request 'Logging on webhooks' (#531) from tg-530 into main
All checks were successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #531
2026-02-19 16:56:41 +00:00
Stephan D
578a2cd1d7 Logging on webhooks 2026-02-19 17:56:17 +01:00
e399e13b56 [infra][rebuild] Merge pull request 'quotation v2 service' (#529) from quote-528 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees 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_mntx 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/nats 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
[infra][rebuild] Reviewed-on: #529
2026-02-18 21:07:44 +00:00
Stephan D
1c5d3d202b quotation v2 service 2026-02-18 22:06:51 +01:00
a33be56247 Merge pull request 'verification before payment and email fixes' (#526) from SEND054 into main
All checks were successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #526
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-18 19:40:58 +00:00
3a310caeb6 Merge pull request 'Fixes + stable gateway ids' (#527) from gate-525 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline is pending
ci/woodpecker/push/discovery Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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/bff Pipeline failed
ci/woodpecker/push/billing_documents Pipeline failed
Reviewed-on: #527
2026-02-18 19:38:50 +00:00
Stephan D
770c7b9da9 Fixes + stable gateway ids 2026-02-18 20:38:08 +01:00
Arseni
e901ac3eb6 verification before payment and email fixes 2026-02-18 18:15:38 +03:00
4dc182bfa2 Merge pull request 'outbox for gateways' (#524) from outbox-523 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/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_tgsettle 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
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
Reviewed-on: #524
2026-02-18 00:36:10 +00:00
Stephan D
69531cee73 outbox for gateways 2026-02-18 01:35:28 +01:00
Stephan D
974caf286c Fixed ORG ledger test
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
2026-02-17 10:30:22 +01:00
854a6af2a6 Merge pull request 'email registration updated' (#519) from reg-516 into main
Some checks failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #519
2026-02-17 09:21:44 +00:00
2707acedae Merge pull request 'reports page' (#518) from SEND053 into main
Some checks failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #518
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-17 09:21:38 +00:00
Stephan D
9bdb667b08 email registration updated 2026-02-17 10:20:39 +01:00
Arseni
e2e2257167 small fixes 2026-02-17 11:17:19 +03:00
Arseni
0eea39fb97 reports page 2026-02-16 21:05:38 +03:00
11d4b9a608 Merge pull request 'switched USDT / USD rate inversion off' (#515) from quote-513 into main
All checks were successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
Reviewed-on: #515
2026-02-16 12:04:18 +00:00
Stephan D
fe3f6ca79a switched USDT / USD rate inversion off 2026-02-16 13:03:54 +01:00
2070c35c61 Merge pull request 'switched USDT / USD rate inversion off' (#514) from quote-513 into main
Some checks failed
ci/woodpecker/push/fx_ingestor Pipeline failed
Reviewed-on: #514
2026-02-16 12:01:29 +00:00
Stephan D
138ab00474 switched USDT / USD rate inversion off 2026-02-16 13:00:51 +01:00
930e8d08cd Merge pull request '[rebuild] proto sync' (#512) from rb-508 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/discovery Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_mntx 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/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: #512
2026-02-13 16:59:30 +00:00
Stephan D
33ea643dc3 [rebuild] 2026-02-13 17:56:18 +01:00
4c51742ec6 Merge pull request 'payment services build deps fix' (#511) from po-510 into main
Reviewed-on: #511
2026-02-13 16:19:19 +00:00
Stephan D
7c76ca7f56 payment services build deps fix 2026-02-13 17:15:26 +01:00
e77baf1687 Merge branch 'dis-474' into main
All checks were successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
2026-02-13 16:05:04 +00:00
2c2512ead8 Merge pull request 'fixed error definition' (#508) from po-507 into main
Some checks failed
ci/woodpecker/push/payments_quotation Pipeline failed
Reviewed-on: #508
2026-02-13 16:01:45 +00:00
Stephan D
5dd1b5f4d7 fixed error definition 2026-02-13 17:01:16 +01:00
de48205e68 Merge pull request 'fixed error definition' (#506) from fx-505 into main
All checks were successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
Reviewed-on: #506
2026-02-13 15:47:38 +00:00
Stephan D
89ac299688 fixed error definition 2026-02-13 16:47:04 +01:00
1c564daa41 Merge pull request 'test fix' (#504) from test-503 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #504
2026-02-13 15:12:26 +00:00
Stephan D
71296800ef test fix 2026-02-13 15:53:52 +01:00
ae873caa57 Merge pull request 'fixed settlement mode import' (#502) from quote-500 into main
Some checks failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #502
2026-02-13 14:42:39 +00:00
Stephan D
01020bb694 fixed settlement mode import 2026-02-13 15:41:42 +01:00
e6232f9b1d Merge pull request 'neq quotation definition + priced_at field' (#501) from quote-500 into main
Some checks failed
ci/woodpecker/push/discovery Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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/bff Pipeline failed
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/billing_documents Pipeline failed
Reviewed-on: #501
2026-02-13 14:35:52 +00:00
Stephan D
52c4c046c9 neq quotation definition + priced_at field 2026-02-13 15:35:17 +01:00
da1636014b Merge pull request 'autotests' (#499) from at-498 into main
Reviewed-on: #499
2026-02-13 11:14:58 +00:00
Stephan D
8704a968d2 autotests 2026-02-13 12:14:38 +01:00
08dccae175 Merge pull request 'certificates fix' (#497) from front-496 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #497
2026-02-13 08:49:41 +00:00
Stephan D
91dc2bd844 certificates fix 2026-02-13 09:49:22 +01:00
f0b14a8bbd Merge pull request 'fixed tests and compilation' (#495) from fx-494 into main
All checks were successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #495
2026-02-13 01:33:51 +00:00
Stephan D
1b3aa0f9ea fixed tests and compilation 2026-02-13 02:33:22 +01:00
71a7a474c8 Merge pull request 'Moved cursor to a separate file' (#493) from cur-492 into main
Some checks failed
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline failed
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline failed
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/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/payments_methods Pipeline failed
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #493
2026-02-12 23:27:12 +00:00
Stephan D
16c44ec7d3 Moved cursor to a separate file 2026-02-13 00:23:39 +01:00
fd1f5498b4 Merge pull request 'fix for resend, cooldown and a few small fixes' (#491) from SEND052 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #491
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-12 23:13:01 +00:00
Arseni
b5db65ef78 fix for resend, cooldown and a few small fixes 2026-02-13 01:03:47 +03:00
44a22ce962 Merge pull request 'new payment methods service' (#490) from pm-489 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline is pending
ci/woodpecker/push/discovery Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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 failed
Reviewed-on: #490
2026-02-12 20:11:56 +00:00
Stephan D
a862e27087 new payment methods service 2026-02-12 21:10:33 +01:00
b80dca0ce9 Merge pull request 'fixed verificaiton error' (#488) from ver-487 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_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/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: #488
2026-02-12 19:46:35 +00:00
Stephan D
e605c734ad fixed verificaiton error 2026-02-12 20:46:11 +01:00
55624aa8b5 Merge pull request 'fixed verificatoin' (#487) from q-477 into main
Some checks failed
ci/woodpecker/push/fx_oracle Pipeline is pending
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_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/billing_fees 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/fx_ingestor Pipeline failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #487
2026-02-12 19:27:04 +00:00
Stephan D
fcbfa323c8 fixed verificatoin 2026-02-12 20:26:10 +01:00
33c83b6768 Merge pull request 'redisign multiple payouts for better ux and small fixes' (#485) from SEND051 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #485
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-12 17:03:53 +00:00
Arseni
45d3c3145c redisign multiple payouts for better ux and small fixes 2026-02-12 18:48:57 +03:00
ea68d161d6 Merge pull request 'localization fix for amount' (#484) from SEND050 into main
All checks were successful
ci/woodpecker/push/payments_quotation Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #484
2026-02-12 13:41:40 +00:00
6a0289963b Merge branch 'main' into SEND050 2026-02-12 13:41:13 +00:00
Arseni
853b855049 localization fix for amount 2026-02-12 16:06:01 +03:00
e767436f33 Merge pull request 'q' (#483) from q-477 into main
All checks were successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #483
2026-02-12 13:05:39 +00:00
Stephan D
57914c0754 q 2026-02-12 14:05:24 +01:00
b894f92e50 Merge pull request 'q' (#482) from q-477 into main
Reviewed-on: #482
2026-02-12 13:04:56 +00:00
Stephan D
76548032b3 q 2026-02-12 14:04:38 +01:00
d121f4172e Merge pull request 'q' (#481) from q-477 into main
All checks were successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #481
2026-02-12 12:53:06 +00:00
Stephan D
e717cabd0f q 2026-02-12 13:52:53 +01:00
8fead967d1 Merge pull request 'q NATS' (#480) from q-477 into main
Reviewed-on: #480
2026-02-12 12:52:04 +00:00
Stephan D
a09fd550ba q NATS 2026-02-12 13:51:49 +01:00
8eae9270f2 Merge pull request 'q NATS' (#479) from q-477 into main
Reviewed-on: #479
2026-02-12 12:51:20 +00:00
Stephan D
dc6a4224a0 q NATS 2026-02-12 13:50:58 +01:00
a99a3c851c Merge pull request 'q' (#478) from q-477 into main
All checks were successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #478
2026-02-12 12:44:26 +00:00
Stephan D
9bca523caa q 2026-02-12 13:44:10 +01:00
Stephan D
e36fb88a9a linting 2026-02-12 13:39:25 +01:00
27de7a9655 Merge pull request 'fixed linting config' (#476) from dis-474 into main
Some checks failed
ci/woodpecker/push/billing_fees Pipeline is pending
ci/woodpecker/push/discovery Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/billing_documents Pipeline failed
ci/woodpecker/push/bff Pipeline failed
Reviewed-on: #476
2026-02-12 12:35:02 +00:00
Stephan D
7cbcbb4b3c fixed linting config 2026-02-12 13:00:37 +01:00
fee10afbb8 Merge pull request 'linting' (#475) from dis-474 into main
All checks were successful
ci/woodpecker/push/discovery Pipeline was successful
Reviewed-on: #475
2026-02-12 11:48:36 +00:00
Stephan D
97395acd8f linting 2026-02-12 12:47:39 +01:00
f4b43f7218 Merge pull request 'fixed bff compilation' (#473) from po-473 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #473
2026-02-11 17:53:49 +00:00
Stephan D
3c0d709a82 fixed bff compilation 2026-02-11 18:53:27 +01:00
b787cc4d9e Merge pull request 'fix for proto migration' (#472) from po-470 into main
Some checks failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #472
2026-02-11 17:16:06 +00:00
Stephan D
7b53ca6cef fix for proto migration 2026-02-11 18:15:04 +01:00
fab07fdc8e Merge pull request 'Fully separated payment quotation and orchestration flows' (#470) from po-469 into main
Some checks failed
ci/woodpecker/push/discovery Pipeline is pending
ci/woodpecker/push/frontend Pipeline is pending
ci/woodpecker/push/fx_ingestor Pipeline is pending
ci/woodpecker/push/fx_oracle Pipeline is pending
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_orchestrator Pipeline is pending
ci/woodpecker/push/payments_quotation Pipeline is pending
ci/woodpecker/push/billing_fees Pipeline failed
ci/woodpecker/push/bff Pipeline failed
ci/woodpecker/push/billing_documents Pipeline failed
Reviewed-on: #470
2026-02-11 16:40:20 +00:00
Stephan D
e116535926 Fully separated payment quotation and orchestration flows 2026-02-11 17:25:44 +01:00
9b8f59e05a Merge pull request 'small fixed for build to run' (#468) from SEND048 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #468
2026-02-11 16:22:41 +00:00
Arseni
6844d1e587 small fixed for build to run 2026-02-11 19:14:37 +03:00
3225babeca [infra] Merge pull request 'multiple payout page and small fixes' (#464) from SEND047 into main
Some checks failed
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/frontend Pipeline failed
[infra]
Reviewed-on: #464
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-11 15:29:28 +00:00
Arseni
0a1e04d0d6 small fixes 2026-02-11 15:26:11 +03:00
648ace709a Merge pull request 'added proto generation commands' (#467) from rm-464 into main
Reviewed-on: #467
2026-02-11 09:40:29 +00:00
Stephan D
274035ed6d added proto generation commands 2026-02-11 10:40:12 +01:00
0aab5cceb0 Merge pull request 'updated badge' (#466) from rm-464 into main
Reviewed-on: #466
2026-02-11 09:33:58 +00:00
Stephan D
9b97ebfa6c updated badge 2026-02-11 10:33:39 +01:00
b02df379a7 Merge pull request 'Readme' (#465) from rm-464 into main
Reviewed-on: #465
2026-02-11 09:31:42 +00:00
Stephan D
f5052f6cf8 Readme 2026-02-11 10:31:17 +01:00
Arseni
edb43f9909 multiple payout page and small fixes 2026-02-11 02:48:30 +03:00
7524852533 Merge pull request 'account state changes' (#463) from alert-444 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/billing_fees 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/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #463
2026-02-10 19:40:39 +00:00
Stephan D
cfb219e206 account state changes 2026-02-10 20:40:23 +01:00
66989ea36c Merge pull request 'separated quotation and payments' (#462) from pq-462 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/discovery 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_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/payments_quotation Pipeline was successful
Reviewed-on: #462
2026-02-10 17:31:21 +00:00
Stephan D
296cc7b86a separated quotation and payments 2026-02-10 18:29:47 +01:00
6745bc0f6f Merge pull request 'sign up page scroll fix' (#461) from SEND046 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #461
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-10 15:05:26 +00:00
Arseni
c962ac7cbd sign up page scroll fix 2026-02-10 17:41:52 +03:00
817d4357cf Merge pull request 'fixed code duplication' (#459) from mntx-452 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #459
2026-02-10 13:40:16 +00:00
Stephan D
2711d601b0 fixed code duplication 2026-02-10 14:39:30 +01:00
74b0976cb7 Merge pull request 'improved storing' (#457) from mntx-452 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #457
2026-02-10 13:20:17 +00:00
Stephan D
3c82afbd43 improved storing 2026-02-10 14:19:44 +01:00
32688a2de7 Merge pull request 'improved storing' (#456) from mntx-452 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #456
2026-02-10 12:37:09 +00:00
Stephan D
f2938daddd improved storing 2026-02-10 13:36:45 +01:00
7c26698f0d Merge pull request 'extended logging' (#454) from mntx-452 into main
All checks were successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/discovery 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_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_orchestrator Pipeline was successful
Reviewed-on: #454
2026-02-10 10:34:18 +00:00
Stephan D
461a340b08 extended logging 2026-02-10 11:33:47 +01:00
dadf9e2485 Merge pull request 'extended logging' (#453) from mntx-452 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #453
2026-02-10 10:25:09 +00:00
Stephan D
f59789ab7a extended logging 2026-02-10 11:24:45 +01:00
5e92d7b9ff Merge pull request 'extended logging' (#452) from mntx-452 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #452
2026-02-10 10:14:24 +00:00
Stephan D
3578ddd54f extended logging 2026-02-10 11:14:02 +01:00
9463c7ce1f Merge pull request 'otp-450' (#451) from otp-450 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/discovery Pipeline was successful
ci/woodpecker/push/fx_oracle Pipeline was successful
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/billing_fees Pipeline was successful
ci/woodpecker/push/fx_ingestor Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
ci/woodpecker/push/gateway_chain Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
Reviewed-on: #451
2026-02-10 01:12:05 +00:00
Stephan D
7c182afd23 fixed token errors 2026-02-10 02:11:22 +01:00
Stephan D
7f540671c1 unified code verification service 2026-02-10 01:55:33 +01:00
Stephan D
76c3bfdea9 fixed verification code
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/discovery Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification 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/frontend 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/payments_orchestrator Pipeline was successful
2026-02-09 16:43:25 +01:00
Stephan D
eda6b75f74 fixed verification code 2026-02-09 16:40:52 +01:00
5caf46ffe1 Merge pull request 'Added debit settlement amount calculation' (#445) from settlement-443 into main
Some checks failed
ci/woodpecker/push/billing_documents Pipeline was successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/billing_fees 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/frontend 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/nats Pipeline failed
ci/woodpecker/push/ledger Pipeline was successful
ci/woodpecker/push/notification Pipeline failed
ci/woodpecker/push/payments_orchestrator Pipeline was successful
[infra] Reviewed-on: #445
2026-02-06 16:50:53 +00:00
Stephan D
c8b8b1183b Added debit settlement amount calculation 2026-02-06 17:50:11 +01:00
17bc2a2a62 Merge pull request 'fixed payment intent request' (#442) from fx-441 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #442
2026-02-05 21:39:39 +00:00
Stephan D
706861af5f fixed payment intent request 2026-02-05 22:39:20 +01:00
Stephan D
f8a3bef2e6 implemented verifiaction db 2026-02-05 20:51:03 +01:00
4639b2c610 Merge pull request 'small fixes for single payout and big chunck for multiple payouts' (#439) from SEND045 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #439
Reviewed-by: tech <tech.sendico@proton.me>
2026-02-05 19:08:02 +00:00
Arseni
b9748b8ab2 small fixes for single payout and big chunck for multiple payouts 2026-02-05 21:58:37 +03:00
8034847e46 Merge pull request 'extended fields' (#438) from bff-437 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #438
2026-02-05 18:04:05 +00:00
Stephan D
49b0b63f55 extended fields 2026-02-05 19:03:35 +01:00
5984f2c2f7 Merge pull request 'fixed upsert' (#435) from mntx-433 into main
All checks were successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #435
2026-02-05 15:35:10 +00:00
Stephan D
bc50391fe7 fixed upsert 2026-02-05 16:34:34 +01:00
67598449e6 Merge pull request 'fixed db operations' (#434) from mntx-433 into main
Some checks failed
ci/woodpecker/push/gateway_mntx Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline failed
Reviewed-on: #434
2026-02-05 15:28:22 +00:00
Stephan D
761dda9377 fixed db operations 2026-02-05 16:27:43 +01:00
42da0260b0 Merge pull request 'fixed status orchestration' (#432) from po-431 into main
All checks were successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #432
2026-02-05 14:34:17 +00:00
Stephan D
5df02baa80 fixed status orchestration 2026-02-05 15:33:41 +01:00
7417b33de3 Merge pull request 'fixed transactions' (#430) from tron-430 into main
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #430
2026-02-05 13:37:00 +00:00
Stephan D
217542ec14 fixed transactions 2026-02-05 14:36:38 +01:00
e394770eb1 Merge pull request 'SEND044' (#429) from SEND044 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #429
2026-02-05 12:53:54 +00:00
Arseni
611bde214f removed podfile 2026-02-05 15:38:48 +03:00
Arseni
d3e69bcd62 removed payment methods page for now 2026-02-05 15:36:43 +03:00
fb9def8c19 Merge pull request 'fixed balance fetch' (#428) from tron-428 into main
All checks were successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #428
2026-02-05 11:39:24 +00:00
Stephan D
6e3115e7fa fixed balance fetch 2026-02-05 12:39:06 +01:00
0675978bd1 Merge pull request 'fixed tron address conversion' (#427) from tron-425 into main
All checks were successful
ci/woodpecker/push/gateway_tron Pipeline was successful
Reviewed-on: #427
2026-02-05 11:24:09 +00:00
Stephan D
04391cbd8d fixed tron address conversion 2026-02-05 12:23:39 +01:00
87f320802d Merge pull request 'TRON driver update' (#426) from tron-425 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/gateway_tron Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #426
2026-02-05 10:48:04 +00:00
Stephan D
3a4f1c7e3f TRON driver update 2026-02-05 11:47:41 +01:00
542d88750d Merge pull request 'name ui fix and removed parts of the app that are not ready' (#422) from SEND043 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #422
2026-02-05 10:05:24 +00:00
Arseni
b27eed31b7 removed status from recipient address book 2026-02-05 13:04:00 +03:00
Arseni
69ed9f25cb deleted single payout that we will not use 2026-02-05 12:02:48 +03:00
Arseni
e4fb270390 fix for signup router 2026-02-05 12:00:05 +03:00
Arseni
81ffdd4291 sneaky email verification fix 2026-02-05 02:54:13 +03:00
Arseni
0ce90eef21 name ui fix and removed parts of the app that are not ready 2026-02-05 02:42:00 +03:00
509af9bc5c Merge pull request 'fixed payments access requireing organization reference' (#419) from po-418 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/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 failed
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was 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/gateway_tron Pipeline was successful
ci/woodpecker/push/ledger Pipeline was successful
Reviewed-on: #419
2026-02-04 20:38:45 +00:00
Stephan D
bc0fd6ab2f fixed payments access requireing organization reference 2026-02-04 21:38:19 +01:00
ca0e45ee5f Merge pull request 'clear instuctions for password in signup' (#413) from SEND042 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #413
2026-02-04 17:31:24 +00:00
3cf6b10ea7 Merge pull request 'fixed gs config' (#415) from bff-414 into main
All checks were successful
ci/woodpecker/push/bff Pipeline was successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #415
2026-02-04 13:13:24 +00:00
Stephan D
1bdbbf65a4 fixed gs config 2026-02-04 14:13:02 +01:00
Arseni
fe9133c206 clear instuctions for password in signup 2026-02-04 16:07:40 +03:00
b722d61c4f Merge pull request 'Fixed default gateway for bff' (#412) from bff-411 into main
Some checks failed
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_orchestrator 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/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 failed
ci/woodpecker/push/frontend Pipeline failed
Reviewed-on: #412
2026-02-04 12:44:36 +00:00
Stephan D
f56a3d4611 Fixed default gateway for bff 2026-02-04 13:44:20 +01:00
afa842ba65 Merge pull request 'fixed tg message' (#410) from tg-409 into main
All checks were successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
ci/woodpecker/push/notification Pipeline was successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
Reviewed-on: #410
2026-02-04 12:31:30 +00:00
Stephan D
ccc9737d1b fixed tg message 2026-02-04 13:31:10 +01:00
a569757b7f Merge pull request 'fixed message' (#406) from mntx-404 into main
All checks were successful
ci/woodpecker/push/frontend Pipeline was successful
Reviewed-on: #406
2026-02-04 11:13:19 +00:00
Stephan D
43b252859e fixed message 2026-02-04 12:13:04 +01:00
e1c439fb85 Merge pull request 'fixed mntx op provisioning' (#405) from tg-403 into main
All checks were successful
ci/woodpecker/push/payments_orchestrator Pipeline was successful
ci/woodpecker/push/gateway_mntx Pipeline was successful
Reviewed-on: #405
2026-02-04 10:59:36 +00:00
Stephan D
571cea3b37 fixed mntx op provisioning 2026-02-04 11:58:30 +01:00
6198ceb9a4 Merge pull request 'tgsettle status optimization, + build optimization' (#404) from tg-403 into main
All checks were successful
ci/woodpecker/push/db Pipeline was successful
ci/woodpecker/push/nats Pipeline was successful
ci/woodpecker/push/gateway_tgsettle Pipeline was successful
Reviewed-on: #404
2026-02-04 09:55:08 +00:00
Stephan D
f02f28d53f tgsettle status optimization, + build optimization 2026-02-04 10:54:45 +01:00
1758 changed files with 99573 additions and 25865 deletions

View File

@@ -4,11 +4,23 @@ matrix:
BFF_DOCKERFILE: ci/prod/compose/bff.dockerfile BFF_DOCKERFILE: ci/prod/compose/bff.dockerfile
BFF_MONGO_SECRET_PATH: sendico/db BFF_MONGO_SECRET_PATH: sendico/db
BFF_API_SECRET_PATH: sendico/api/endpoint BFF_API_SECRET_PATH: sendico/api/endpoint
BFF_VAULT_SECRET_PATH: sendico/edge/bff/vault
BFF_ENV: prod BFF_ENV: prod
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/edge/bff/**
- api/payments/methods/client/**
- api/payments/methods/go.mod
- api/payments/methods/go.sum
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/bff.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -35,6 +47,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh bff
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -55,7 +75,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/bff/build-image.sh - sh ci/scripts/bff/build-image.sh

View File

@@ -8,6 +8,14 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/billing/documents/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/billing_documents.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -34,6 +42,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh billing_documents
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -54,7 +70,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/billing_documents/build-image.sh - sh ci/scripts/billing_documents/build-image.sh

View File

@@ -8,6 +8,14 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/billing/fees/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/billing_fees.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -34,6 +42,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh billing_fees
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -54,7 +70,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/billing_fees/build-image.sh - sh ci/scripts/billing_fees/build-image.sh

90
.woodpecker/callbacks.yml Normal file
View File

@@ -0,0 +1,90 @@
matrix:
include:
- CALLBACKS_IMAGE_PATH: edge/callbacks
CALLBACKS_DOCKERFILE: ci/prod/compose/callbacks.dockerfile
CALLBACKS_MONGO_SECRET_PATH: sendico/db
CALLBACKS_VAULT_SECRET_PATH: sendico/edge/callbacks/vault
CALLBACKS_ENV: prod
when:
- event: push
branch: main
path:
include:
- api/edge/callbacks/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/callbacks.yml
ignore_message: '[rebuild]'
steps:
- name: version
image: alpine:latest
commands:
- set -euo pipefail 2>/dev/null || set -eu
- apk add --no-cache git
- GIT_REV="$(git rev-parse --short HEAD)"
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
- APP_V="$(cat version)"
- BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}"
- printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \
"$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version
- name: proto
image: golang:alpine
depends_on: [ version ]
commands:
- set -eu
- apk add --no-cache bash git build-base protoc protobuf-dev
- go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh callbacks
- name: secrets
image: alpine:latest
depends_on: [ version ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash coreutils openssh-keygen curl sed python3
- mkdir -p secrets
- ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600
- base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY
- chmod 600 secrets/SSH_KEY
- ssh-keygen -y -f secrets/SSH_KEY >/dev/null
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
commands:
- sh ci/scripts/callbacks/build-image.sh
- name: deploy
image: alpine:latest
depends_on: [ secrets, build-image ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash openssh-client rsync coreutils curl sed python3
- mkdir -p /root/.ssh
- install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa
- sh ci/scripts/callbacks/deploy.sh

View File

@@ -1,6 +1,9 @@
when: when:
- event: push - event: push
branch: main branch: main
path:
exclude: ['**']
ignore_message: '[infra]'
steps: steps:
- name: version - name: version

View File

@@ -7,6 +7,14 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/discovery/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/discovery.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -33,6 +41,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh discovery
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -53,7 +69,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/discovery/build-image.sh - sh ci/scripts/discovery/build-image.sh

View File

@@ -7,6 +7,16 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/edge/bff/**
- api/pkg/**
- api/proto/**
- frontend/**
- interface/**
- ci/prod/**
- .woodpecker/frontend.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version

View File

@@ -11,6 +11,15 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/fx/ingestor/**
- api/fx/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/fx_ingestor.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -38,6 +47,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh fx_ingestor
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -58,7 +75,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/fx/build-image.sh - sh ci/scripts/fx/build-image.sh

View File

@@ -11,6 +11,16 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/fx/oracle/**
- api/fx/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/fx_oracle.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -38,6 +48,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh fx_oracle
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -58,7 +76,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/fx/build-image.sh - sh ci/scripts/fx/build-image.sh

View File

@@ -11,6 +11,15 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/chain/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_chain.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -37,6 +46,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_chain
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -57,7 +74,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/chain_gateway/build-image.sh - sh ci/scripts/chain_gateway/build-image.sh

View File

@@ -10,6 +10,15 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/mntx/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_mntx.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -36,6 +45,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_mntx
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -56,7 +73,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/mntx/build-image.sh - sh ci/scripts/mntx/build-image.sh

View File

@@ -8,6 +8,15 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/tgsettle/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_tgsettle.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -34,6 +43,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_tgsettle
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -54,7 +71,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/tgsettle/build-image.sh - sh ci/scripts/tgsettle/build-image.sh

View File

@@ -11,6 +11,15 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/gateway/tron/**
- api/gateway/common/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/gateway_tron.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -37,6 +46,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh gateway_tron
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -57,7 +74,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/tron_gateway/build-image.sh - sh ci/scripts/tron_gateway/build-image.sh

View File

@@ -8,6 +8,14 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/ledger/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/ledger.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -34,6 +42,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh ledger
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -54,7 +70,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/ledger/build-image.sh - sh ci/scripts/ledger/build-image.sh

View File

@@ -1,6 +1,10 @@
when: when:
- event: push - event: push
branch: main branch: main
path:
exclude: ['**']
ignore_message: '[infra]'
steps: steps:
- name: version - name: version

View File

@@ -11,6 +11,14 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/notification/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/notification.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -37,6 +45,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh notification
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -57,7 +73,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/notification/build-image.sh - sh ci/scripts/notification/build-image.sh

View File

@@ -0,0 +1,90 @@
matrix:
include:
- PAYMENTS_METHODS_IMAGE_PATH: payments/methods
PAYMENTS_METHODS_DOCKERFILE: ci/prod/compose/payments_methods.dockerfile
PAYMENTS_METHODS_MONGO_SECRET_PATH: sendico/db
PAYMENTS_METHODS_ENV: prod
when:
- event: push
branch: main
path:
include:
- api/payments/methods/**
- api/payments/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/payments_methods.yml
ignore_message: '[rebuild]'
steps:
- name: version
image: alpine:latest
commands:
- set -euo pipefail 2>/dev/null || set -eu
- apk add --no-cache git
- GIT_REV="$(git rev-parse --short HEAD)"
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
- APP_V="$(cat version)"
- BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}"
- printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \
"$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version
- name: proto
image: golang:alpine
depends_on: [ version ]
commands:
- set -eu
- apk add --no-cache bash git build-base protoc protobuf-dev
- go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh payments_methods
- name: secrets
image: alpine:latest
depends_on: [ version ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash coreutils openssh-keygen curl sed python3
- mkdir -p secrets
- ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600
- base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY
- chmod 600 secrets/SSH_KEY
- ssh-keygen -y -f secrets/SSH_KEY >/dev/null
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
commands:
- sh ci/scripts/payments_methods/build-image.sh
- name: deploy
image: alpine:latest
depends_on: [ secrets, build-image ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash openssh-client rsync coreutils curl sed python3
- mkdir -p /root/.ssh
- install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa
- sh ci/scripts/payments_methods/deploy.sh

View File

@@ -8,6 +8,15 @@ matrix:
when: when:
- event: push - event: push
branch: main branch: main
path:
include:
- api/payments/orchestrator/**
- api/payments/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/payments_orchestrator.yml
ignore_message: '[rebuild]'
steps: steps:
- name: version - name: version
@@ -34,6 +43,14 @@ 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-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh payments_orchestrator
- name: secrets - name: secrets
image: alpine:latest image: alpine:latest
depends_on: [ version ] depends_on: [ version ]
@@ -54,7 +71,7 @@ steps:
- name: build-image - name: build-image
image: gcr.io/kaniko-project/executor:debug image: gcr.io/kaniko-project/executor:debug
depends_on: [ proto, secrets ] depends_on: [ backend-tests, secrets ]
commands: commands:
- sh ci/scripts/payments_orchestrator/build-image.sh - sh ci/scripts/payments_orchestrator/build-image.sh

View File

@@ -0,0 +1,90 @@
matrix:
include:
- PAYMENTS_QUOTATION_IMAGE_PATH: payments/quotation
PAYMENTS_QUOTATION_DOCKERFILE: ci/prod/compose/payments_quotation.dockerfile
PAYMENTS_QUOTATION_MONGO_SECRET_PATH: sendico/db
PAYMENTS_QUOTATION_ENV: prod
when:
- event: push
branch: main
path:
include:
- api/payments/quotation/**
- api/payments/storage/**
- api/proto/**
- api/pkg/**
- ci/prod/**
- .woodpecker/payments_quotation.yml
ignore_message: '[rebuild]'
steps:
- name: version
image: alpine:latest
commands:
- set -euo pipefail 2>/dev/null || set -eu
- apk add --no-cache git
- GIT_REV="$(git rev-parse --short HEAD)"
- BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)"
- APP_V="$(cat version)"
- BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)"
- BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}"
- printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \
"$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version
- name: proto
image: golang:alpine
depends_on: [ version ]
commands:
- set -eu
- apk add --no-cache bash git build-base protoc protobuf-dev
- go install google.golang.org/protobuf/cmd/protoc-gen-go@latest
- go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest
- export PATH="$(go env GOPATH)/bin:$PATH"
- bash ci/scripts/proto/generate.sh
- name: backend-tests
image: golang:alpine
depends_on: [ proto ]
commands:
- set -eu
- apk add --no-cache bash git build-base
- sh ci/scripts/common/run_backend_tests.sh payments_quotation
- name: secrets
image: alpine:latest
depends_on: [ version ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash coreutils openssh-keygen curl sed python3
- mkdir -p secrets
- ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600
- base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY
- chmod 600 secrets/SSH_KEY
- ssh-keygen -y -f secrets/SSH_KEY >/dev/null
- ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER
- ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD
- name: build-image
image: gcr.io/kaniko-project/executor:debug
depends_on: [ backend-tests, secrets ]
commands:
- sh ci/scripts/payments_quotation/build-image.sh
- name: deploy
image: alpine:latest
depends_on: [ secrets, build-image ]
environment:
VAULT_ADDR: { from_secret: VAULT_ADDR }
VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE }
VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID }
commands:
- set -euo pipefail
- apk add --no-cache bash openssh-client rsync coreutils curl sed python3
- mkdir -p /root/.ssh
- install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa
- sh ci/scripts/payments_quotation/deploy.sh

View File

@@ -1,7 +1,7 @@
# 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 .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
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 ?=
@@ -38,11 +38,20 @@ help:
@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 orchestrator"
@echo " make build-gateways Build gateway services (chain, tron, mntx, tgsettle)" @echo " make build-gateways Build gateway services (chain, tron, mntx, tgsettle)"
@echo " make build-api Build API services (notification, bff)" @echo " make build-api Build API 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-api Generate protobuf code only"
@echo " make generate-frontend Generate Flutter code only"
@echo " make update Update all dependencies (Go + Flutter)"
@echo " make update-api Update Go dependencies only"
@echo " make update-frontend Update Flutter dependencies only"
@echo " make test Run all tests (API + frontend)"
@echo " make test-api Run Go API tests only"
@echo " make test-frontend Run Flutter tests only"
@echo " make health Check service health" @echo " make health Check service health"
@echo "" @echo ""
@echo "Examples:" @echo "Examples:"
@@ -76,7 +85,7 @@ init:
@echo "$(GREEN)Verifying .env.dev...$(NC)" @echo "$(GREEN)Verifying .env.dev...$(NC)"
@cat .env.dev | grep -q "MONGO_USER=" || (echo "$(YELLOW)Error: .env.dev is incomplete$(NC)" && exit 1) @cat .env.dev | grep -q "MONGO_USER=" || (echo "$(YELLOW)Error: .env.dev is incomplete$(NC)" && exit 1)
@echo "$(GREEN)Running proto generation...$(NC)" @echo "$(GREEN)Running proto generation...$(NC)"
@./generate_protos.sh @./ci/scripts/proto/generate.sh
@echo "$(GREEN)Building Docker images...$(NC)" @echo "$(GREEN)Building Docker images...$(NC)"
@$(COMPOSE) build @$(COMPOSE) build
@echo "$(GREEN)✅ Initialization complete!$(NC)" @echo "$(GREEN)✅ Initialization complete!$(NC)"
@@ -88,7 +97,7 @@ init:
# Build all images # Build all images
build: build:
@echo "$(GREEN)Building all service images...$(NC)" @echo "$(GREEN)Building all service images...$(NC)"
@./generate_protos.sh @./ci/scripts/proto/generate.sh
@$(COMPOSE) build @$(COMPOSE) build
# Start all services # Start all services
@@ -136,12 +145,25 @@ endif
@echo "$(GREEN)✅ $(SERVICE) rebuilt$(NC)" @echo "$(GREEN)✅ $(SERVICE) rebuilt$(NC)"
@echo "View logs: make logs SERVICE=$(SERVICE)" @echo "View logs: make logs SERVICE=$(SERVICE)"
# Generate protobuf code (alias)
proto: generate-api
# Generate all code
generate: generate-api generate-frontend
# Generate protobuf code # Generate protobuf code
proto: generate-api:
@echo "$(GREEN)Generating protobuf code...$(NC)" @echo "$(GREEN)Generating protobuf code...$(NC)"
@./generate_protos.sh @./ci/scripts/proto/generate.sh
@echo "$(GREEN)✅ Protobuf generation complete$(NC)" @echo "$(GREEN)✅ Protobuf generation complete$(NC)"
# Generate Flutter code (json_serializable, etc.)
generate-frontend:
@echo "$(GREEN)Generating Flutter code...$(NC)"
@cd frontend/pshared && dart run build_runner build --delete-conflicting-outputs
@cd frontend/pweb && dart run build_runner build --delete-conflicting-outputs
@echo "$(GREEN)✅ Flutter code generation complete$(NC)"
# Clean everything # Clean everything
clean: clean:
@echo "$(YELLOW)WARNING: This will remove all containers and volumes!$(NC)" @echo "$(YELLOW)WARNING: This will remove all containers and volumes!$(NC)"
@@ -196,11 +218,14 @@ services-up:
dev-billing-documents \ dev-billing-documents \
dev-ledger \ dev-ledger \
dev-payments-orchestrator \ dev-payments-orchestrator \
dev-payments-quotation \
dev-payments-methods \
dev-chain-gateway \ dev-chain-gateway \
dev-tron-gateway \ dev-tron-gateway \
dev-mntx-gateway \ dev-mntx-gateway \
dev-tgsettle-gateway \ dev-tgsettle-gateway \
dev-notification \ dev-notification \
dev-callbacks \
dev-bff \ dev-bff \
dev-frontend dev-frontend
@@ -223,11 +248,14 @@ list-services:
@echo " - dev-billing-documents :50061, :9409 (Billing Documents)" @echo " - dev-billing-documents :50061, :9409 (Billing Documents)"
@echo " - dev-ledger :50052, :9401 (Double-Entry Ledger)" @echo " - dev-ledger :50052, :9401 (Double-Entry Ledger)"
@echo " - dev-payments-orchestrator :50062, :9403 (Payment Orchestration)" @echo " - dev-payments-orchestrator :50062, :9403 (Payment Orchestration)"
@echo " - dev-payments-quotation :50064, :9414 (Payment Quotation)"
@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-mntx-gateway :50075, :9405, :8084 (Card Payouts)"
@echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)" @echo " - dev-tgsettle-gateway :50080, :9406 (Telegram Settlements)"
@echo " - dev-notification :8081 (Notifications)" @echo " - dev-notification :8081 (Notifications)"
@echo " - dev-callbacks :9420 (Webhook Callbacks)"
@echo " - dev-bff :8080 (Backend for Frontend)" @echo " - dev-bff :8080 (Backend for Frontend)"
@echo " - dev-frontend :3000 (Flutter Web UI)" @echo " - dev-frontend :3000 (Flutter Web UI)"
@@ -251,7 +279,7 @@ build-fx:
build-payments: build-payments:
@echo "$(GREEN)Building payment services...$(NC)" @echo "$(GREEN)Building payment services...$(NC)"
@$(COMPOSE) build dev-payments-orchestrator @$(COMPOSE) build dev-payments-orchestrator dev-payments-quotation dev-payments-methods
build-gateways: build-gateways:
@echo "$(GREEN)Building gateway services...$(NC)" @echo "$(GREEN)Building gateway services...$(NC)"
@@ -259,8 +287,51 @@ build-gateways:
build-api: build-api:
@echo "$(GREEN)Building API services...$(NC)" @echo "$(GREEN)Building API services...$(NC)"
@$(COMPOSE) build dev-notification dev-bff @$(COMPOSE) build dev-notification dev-callbacks dev-bff
build-frontend: build-frontend:
@echo "$(GREEN)Building frontend...$(NC)" @echo "$(GREEN)Building frontend...$(NC)"
@$(COMPOSE) build dev-frontend @$(COMPOSE) build dev-frontend
# Update all dependencies
update: update-api update-frontend
# Update Go API dependencies
update-api:
@echo "$(GREEN)Updating Go dependencies...$(NC)"
@for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Updating $$dir..."; \
(cd "$$dir" && go get -u ./... && go mod tidy); \
done
@echo "$(GREEN)✅ Go dependencies updated$(NC)"
# Update Flutter dependencies
update-frontend:
@echo "$(GREEN)Updating Flutter dependencies...$(NC)"
@cd frontend/pshared && flutter pub upgrade --major-versions
@cd frontend/pweb && flutter pub upgrade --major-versions
@echo "$(GREEN)✅ Flutter dependencies updated$(NC)"
# Run all tests
test: test-api test-frontend
# Run Go API tests
test-api:
@echo "$(GREEN)Running API tests...$(NC)"
@failed=""; \
for dir in $$(find api -name go.mod -exec dirname {} \;); do \
echo "Testing $$dir..."; \
(cd "$$dir" && go test ./...) || failed="$$failed $$dir"; \
done; \
if [ -n "$$failed" ]; then \
echo "$(YELLOW)Failed:$$failed$(NC)"; \
exit 1; \
fi
@echo "$(GREEN)✅ All API tests passed$(NC)"
# Run Flutter tests
test-frontend:
@echo "$(GREEN)Running frontend tests...$(NC)"
@cd frontend/pshared && flutter test
@cd frontend/pweb && flutter test
@echo "$(GREEN)✅ All frontend tests passed$(NC)"

162
README.md Normal file
View File

@@ -0,0 +1,162 @@
# Sendico [![Build Status](https://ci.sendico.io/api/badges/1/status.svg?branch=main)](https://ci.sendico.io/repos/1)
Financial services platform providing payment orchestration, ledger accounting, FX conversion, and multi-rail payment processing.
## Architecture
- **Backend**: Go microservices with gRPC inter-service communication
- **Frontend**: Flutter/Dart web application
- **Infrastructure**: Woodpecker CI/CD, Docker, MongoDB, NATS, Vault
## Services
| Service | Path | Description |
|---------|------|-------------|
| Discovery | `api/discovery/` | Service registry |
| Ledger | `api/ledger/` | Double-entry accounting |
| Orchestrator | `api/payments/orchestrator/` | Payment orchestration |
| Quotation | `api/payments/quotation/` | Payment quotation |
| Payment Methods | `api/payments/methods/` | Payment methods |
| Billing Fees | `api/billing/fees/` | Fee calculation |
| Billing Documents | `api/billing/documents/` | Billing documents |
| FX Oracle | `api/fx/oracle/` | FX quote provider |
| FX Ingestor | `api/fx/ingestor/` | FX rate ingestion |
| Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway |
| Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway |
| Gateway MNTX | `api/gateway/mntx/` | Card payouts |
| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX |
| Notification | `api/notification/` | Notifications |
| BFF | `api/edge/bff/` | Backend for frontend |
| Callbacks | `api/edge/callbacks/` | Webhook callbacks delivery |
| Frontend | `frontend/pweb/` | Flutter web UI |
## Development
Development uses Docker Compose via the Makefile. Run `make help` for all available commands.
### Quick Start
```bash
make init # First-time setup (generates keys, .env.dev, builds images)
make up # Start all services
make vault-init # Initialize Vault (if needed)
```
### Common Commands
```bash
make build # Build all service images
make up # Start all services
make down # Stop all services
make restart # Restart all services
make status # Show service status
make logs # View all logs
make logs SERVICE=dev-ledger # View logs for a specific service
make rebuild SERVICE=dev-ledger # Rebuild and restart a specific service
make clean # Remove all containers and volumes
```
### Selective Start
```bash
make infra-up # Start infrastructure only (MongoDB, NATS, Vault)
make services-up # Start application services only (assumes infra is running)
```
### Build Groups
```bash
make build-core # discovery, ledger, fees, documents
make build-fx # oracle, ingestor
make build-payments # orchestrator
make build-gateways # chain, tron, mntx, tgsettle
make build-api # notification, callbacks, bff
make build-frontend # Flutter web UI
```
### Code Generation
```bash
make generate # Generate all code (protobuf + Flutter)
make generate-api # Generate protobuf code only
make generate-frontend # Generate Flutter code only (build_runner)
make proto # Alias for generate-api
```
### Testing
```bash
make test # Run all tests (API + frontend)
make test-api # Run Go API tests only
make test-frontend # Run Flutter tests only
```
### Update Dependencies
```bash
make update # Update all Go and Flutter dependencies
make update-api # Update Go dependencies only
make update-frontend # Update Flutter dependencies only
```
### Callbacks Secret References
Callbacks (`api/edge/callbacks`) supports three secret reference formats:
- `env:MY_SECRET_ENV` to read from environment variables.
- `vault:some/path#field` to read a field from Vault KV v2.
- `some/path#field` to read from Vault KV v2 when `secrets.vault` is configured.
If `#field` is omitted, callbacks uses `secrets.vault.default_field` (default: `value`).
### Callbacks Vault Auth (Dev + Prod)
Callbacks now authenticates to Vault through a sidecar Vault Agent (AppRole), same pattern as chain/tron gateways.
- Dev compose:
- service: `dev-callbacks-vault-agent`
- shared token file: `/run/vault/token`
- app reads token via `VAULT_TOKEN_FILE=/run/vault/token` and `token_env: VAULT_TOKEN`
- Prod compose:
- service: `sendico_callbacks_vault_agent`
- same token sink and env flow
- AppRole creds are injected at deploy from `CALLBACKS_VAULT_SECRET_PATH` (default `sendico/edge/callbacks/vault`)
Required Vault policy (minimal read-only for KV v2 mount `kv`):
```hcl
path "kv/data/sendico/callbacks/*" {
capabilities = ["read"]
}
path "kv/metadata/sendico/callbacks/*" {
capabilities = ["read", "list"]
}
```
Create policy + role (example):
```bash
vault policy write callbacks callbacks-policy.hcl
vault write auth/approle/role/callbacks \
token_policies="callbacks" \
token_ttl="1h" \
token_max_ttl="24h"
vault read -field=role_id auth/approle/role/callbacks/role-id
vault write -f -field=secret_id auth/approle/role/callbacks/secret-id
```
Store AppRole creds for prod deploy pipeline:
```bash
vault kv put kv/sendico/edge/callbacks/vault \
role_id="<callbacks-role-id>" \
secret_id="<callbacks-secret-id>"
```
Store webhook signing secrets (example path consumed by `secret_ref`):
```bash
vault kv put kv/sendico/callbacks/client-a/webhook secret="super-secret"
```

View File

@@ -0,0 +1,196 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# 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:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- wsl
- wrapcheck
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# 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

@@ -1,70 +1,70 @@
module github.com/tech/sendico/billing/documents module github.com/tech/sendico/billing/documents
go 1.25.6 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.1 github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3
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.78.0 google.golang.org/grpc v1.79.1
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.4 // 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.17 // 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.17 // 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.17 // 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.17 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // 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.8 // 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.17 // 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.17 // 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.5 // 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.9 // 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.13 // 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.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.0 // 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
github.com/casbin/casbin/v2 v2.135.0 // indirect github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.4 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
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.3 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -4,44 +4,44 @@ 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.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= 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.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= 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.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= 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.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= 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.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= 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.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= 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.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= 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.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= 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.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= 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.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= 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.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= 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.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= 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.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= 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.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= 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.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= 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.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= 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.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= 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.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= 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.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= 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.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= 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.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= 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.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= 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.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= 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.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= 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.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= 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.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= 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.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= 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.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 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=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc=
@@ -78,8 +78,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -100,8 +100,8 @@ github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+
github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes= github.com/jung-kurt/gofpdf v1.0.0/go.mod h1:7Id9E/uU8ce6rXgefFLlgrJj/GYY22cpxn+r32jIOes=
github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc= github.com/jung-kurt/gofpdf v1.16.2 h1:jgbatWHfRlPYiK85qgevsZTHviWXKwB1TTiKdz5PtRc=
github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0= github.com/jung-kurt/gofpdf v1.16.2/go.mod h1:1hl7y57EsiPAkLbOwzpzqgx1A30nQCk/YmFV8S2vmK0=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -134,8 +134,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -158,8 +158,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w= github.com/ruudk/golang-pdf417 v0.0.0-20181029194003-1af4ab5afa58/go.mod h1:6lfFZQK844Gfx8o5WFuvpxWRwnSoipWe/p622j1v06w=
@@ -201,16 +201,16 @@ 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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -221,16 +221,16 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -241,16 +241,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/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

@@ -24,5 +24,6 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&info) return vf.Create(&info)
} }

View File

@@ -21,11 +21,13 @@ func NewLocalStore(logger mlogger.Logger, cfg LocalConfig) (*LocalStore, error)
if root == "" { if root == "" {
return nil, merrors.InvalidArgument("docstore: local root_path is empty") return nil, merrors.InvalidArgument("docstore: local root_path is empty")
} }
store := &LocalStore{ store := &LocalStore{
logger: logger.Named("docstore").Named("local"), logger: logger.Named("docstore").Named("local"),
rootPath: root, rootPath: root,
} }
store.logger.Info("Document storage initialised", zap.String("root_path", root)) store.logger.Info("Document storage initialised", zap.String("root_path", root))
return store, nil return store, nil
} }
@@ -33,15 +35,19 @@ func (s *LocalStore) Save(ctx context.Context, key string, data []byte) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
path := filepath.Join(s.rootPath, filepath.Clean(key)) path := filepath.Join(s.rootPath, filepath.Clean(key))
if err := os.MkdirAll(filepath.Dir(path), 0o755); err != nil { if err := os.MkdirAll(filepath.Dir(path), 0o755); 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
} }
if err := os.WriteFile(path, data, 0o600); err != nil { if err := os.WriteFile(path, data, 0o600); err != nil {
s.logger.Warn("Failed to write document file", zap.Error(err), zap.String("path", path)) s.logger.Warn("Failed to write document file", zap.Error(err), zap.String("path", path))
return err return err
} }
return nil return nil
} }
@@ -49,12 +55,16 @@ func (s *LocalStore) Load(ctx context.Context, key string) ([]byte, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
path := filepath.Join(s.rootPath, filepath.Clean(key)) path := filepath.Join(s.rootPath, filepath.Clean(key))
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))
return nil, err return nil, err
} }
return data, nil return data, nil
} }

View File

@@ -32,6 +32,7 @@ func NewS3Store(logger mlogger.Logger, cfg S3Config) (*S3Store, error) {
if accessKey == "" && cfg.AccessKeyEnv != "" { if accessKey == "" && cfg.AccessKeyEnv != "" {
accessKey = strings.TrimSpace(os.Getenv(cfg.AccessKeyEnv)) accessKey = strings.TrimSpace(os.Getenv(cfg.AccessKeyEnv))
} }
secretKey := strings.TrimSpace(cfg.SecretAccessKey) secretKey := strings.TrimSpace(cfg.SecretAccessKey)
if secretKey == "" && cfg.SecretKeyEnv != "" { if secretKey == "" && cfg.SecretKeyEnv != "" {
secretKey = strings.TrimSpace(os.Getenv(cfg.SecretKeyEnv)) secretKey = strings.TrimSpace(os.Getenv(cfg.SecretKeyEnv))
@@ -62,23 +63,21 @@ func NewS3Store(logger mlogger.Logger, cfg S3Config) (*S3Store, error) {
endpoint = "http://" + endpoint endpoint = "http://" + endpoint
} }
} }
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, _ ...interface{}) (aws.Endpoint, error) {
if service == s3.ServiceID {
return aws.Endpoint{URL: endpoint, SigningRegion: region, HostnameImmutable: true}, nil
}
return aws.Endpoint{}, &aws.EndpointNotFoundError{}
})
loadOpts = append(loadOpts, config.WithEndpointResolverWithOptions(resolver))
} }
awsCfg, err := config.LoadDefaultConfig(context.Background(), loadOpts...) awsCfg, err := config.LoadDefaultConfig(context.Background(), loadOpts...)
if err != nil { if err != nil {
logger.Warn("Failed to create AWS config", zap.Error(err), zap.String("bucket", bucket)) logger.Warn("Failed to create AWS config", zap.Error(err), zap.String("bucket", bucket))
return nil, err return nil, err
} }
client := s3.NewFromConfig(awsCfg, func(opts *s3.Options) { client := s3.NewFromConfig(awsCfg, func(opts *s3.Options) {
opts.UsePathStyle = cfg.ForcePathStyle opts.UsePathStyle = cfg.ForcePathStyle
if endpoint != "" {
opts.BaseEndpoint = aws.String(endpoint)
}
}) })
store := &S3Store{ store := &S3Store{
@@ -87,6 +86,7 @@ func NewS3Store(logger mlogger.Logger, cfg S3Config) (*S3Store, error) {
bucket: bucket, bucket: bucket,
} }
store.logger.Info("Document storage initialised", zap.String("bucket", bucket), zap.String("endpoint", endpoint)) store.logger.Info("Document storage initialised", zap.String("bucket", bucket), zap.String("endpoint", endpoint))
return store, nil return store, nil
} }
@@ -94,6 +94,7 @@ func (s *S3Store) Save(ctx context.Context, key string, data []byte) error {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return err return err
} }
_, err := s.client.PutObject(ctx, &s3.PutObjectInput{ _, err := s.client.PutObject(ctx, &s3.PutObjectInput{
Bucket: aws.String(s.bucket), Bucket: aws.String(s.bucket),
Key: aws.String(key), Key: aws.String(key),
@@ -101,8 +102,10 @@ func (s *S3Store) Save(ctx context.Context, key string, data []byte) error {
}) })
if err != nil { if err != nil {
s.logger.Warn("Failed to upload document", zap.Error(err), zap.String("key", key)) s.logger.Warn("Failed to upload document", zap.Error(err), zap.String("key", key))
return err return err
} }
return nil return nil
} }
@@ -110,15 +113,19 @@ func (s *S3Store) Load(ctx context.Context, key string) ([]byte, error) {
if err := ctx.Err(); err != nil { if err := ctx.Err(); err != nil {
return nil, err return nil, err
} }
obj, err := s.client.GetObject(ctx, &s3.GetObjectInput{ obj, err := s.client.GetObject(ctx, &s3.GetObjectInput{
Bucket: aws.String(s.bucket), Bucket: aws.String(s.bucket),
Key: aws.String(key), Key: aws.String(key),
}) })
if err != nil { if err != nil {
s.logger.Warn("Failed to fetch document", zap.Error(err), zap.String("key", key)) s.logger.Warn("Failed to fetch document", zap.Error(err), zap.String("key", key))
return nil, err return nil, err
} }
defer obj.Body.Close() defer obj.Body.Close()
return io.ReadAll(obj.Body) return io.ReadAll(obj.Body)
} }

View File

@@ -36,7 +36,7 @@ type S3Config struct {
Bucket string `yaml:"bucket"` Bucket string `yaml:"bucket"`
AccessKeyEnv string `yaml:"access_key_env"` AccessKeyEnv string `yaml:"access_key_env"`
SecretKeyEnv string `yaml:"secret_access_key_env"` SecretKeyEnv string `yaml:"secret_access_key_env"`
AccessKey string `yaml:"access_key"` AccessKey string `yaml:"access_key"` //nolint:gosec // config field, not a hardcoded secret
SecretAccessKey string `yaml:"secret_access_key"` SecretAccessKey string `yaml:"secret_access_key"`
UseSSL bool `yaml:"use_ssl"` UseSSL bool `yaml:"use_ssl"`
ForcePathStyle bool `yaml:"force_path_style"` ForcePathStyle bool `yaml:"force_path_style"`
@@ -55,11 +55,13 @@ func New(logger mlogger.Logger, cfg Config) (Store, error) {
if cfg.Local == nil { if cfg.Local == nil {
return nil, merrors.InvalidArgument("docstore: local config missing") return nil, merrors.InvalidArgument("docstore: local config missing")
} }
return NewLocalStore(logger, *cfg.Local) return NewLocalStore(logger, *cfg.Local)
case string(DriverS3), string(DriverMinio): case string(DriverS3), string(DriverMinio):
if cfg.S3 == nil { if cfg.S3 == nil {
return nil, merrors.InvalidArgument("docstore: s3 config missing") return nil, merrors.InvalidArgument("docstore: s3 config missing")
} }
return NewS3Store(logger, *cfg.S3) return NewS3Store(logger, *cfg.S3)
default: default:
return nil, merrors.InvalidArgument("docstore: unsupported driver") return nil, merrors.InvalidArgument("docstore: unsupported driver")

View File

@@ -29,7 +29,8 @@ type Imp struct {
type config struct { type config struct {
*grpcapp.Config `yaml:",inline"` *grpcapp.Config `yaml:",inline"`
Documents documents.Config `yaml:"documents"`
Documents documents.Config `yaml:"documents"`
} }
// Create initialises the billing documents server implementation. // Create initialises the billing documents server implementation.
@@ -46,6 +47,7 @@ func (i *Imp) Shutdown() {
if i.service != nil { if i.service != nil {
i.service.Shutdown() i.service.Shutdown()
} }
return return
} }
@@ -68,6 +70,7 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
@@ -77,20 +80,23 @@ func (i *Imp) Start() error {
docStore, err := docstore.New(i.logger, cfg.Documents.Storage) docStore, err := docstore.New(i.logger, cfg.Documents.Storage)
if err != nil { if err != nil {
i.logger.Error("Failed to initialise document storage", zap.Error(err)) i.logger.Error("Failed to initialise document storage", zap.Error(err))
return err return err
} }
serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { //nolint:lll // factory signature dictated by grpcapp
invokeURI := "" invokeURI := ""
if cfg.GRPC != nil { if cfg.GRPC != nil {
invokeURI = cfg.GRPC.DiscoveryInvokeURI() invokeURI = cfg.GRPC.DiscoveryInvokeURI()
} }
svc := documents.NewService(logger, repo, producer, svc := documents.NewService(logger, repo, producer,
documents.WithDiscoveryInvokeURI(invokeURI), documents.WithDiscoveryInvokeURI(invokeURI),
documents.WithConfig(cfg.Documents), documents.WithConfig(cfg.Documents),
documents.WithDocumentStore(docStore), documents.WithDocumentStore(docStore),
) )
i.service = svc i.service = svc
return svc, nil return svc, nil
} }
@@ -98,6 +104,7 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.app = app i.app = app
return i.app.Start() return i.app.Start()
@@ -107,12 +114,14 @@ func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err return nil, err
} }
cfg := &config{Config: &grpcapp.Config{}} cfg := &config{Config: &grpcapp.Config{}}
if err := yaml.Unmarshal(data, cfg); err != nil { if err := yaml.Unmarshal(data, cfg); err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err return nil, err
} }

View File

@@ -29,5 +29,6 @@ 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"
} }
return c.Templates.AcceptancePath return c.Templates.AcceptancePath
} }

View File

@@ -85,14 +85,18 @@ func statusFromError(err error) string {
if err == nil { if err == nil {
return "success" return "success"
} }
st, ok := status.FromError(err) st, ok := status.FromError(err)
if !ok { if !ok {
return "error" return "error"
} }
code := st.Code() code := st.Code()
if code == codes.OK { if code == codes.OK {
return "success" return "success"
} }
return strings.ToLower(code.String()) return strings.ToLower(code.String())
} }
@@ -101,5 +105,6 @@ func docTypeLabel(docType documentsv1.DocumentType) string {
if label == "" { if label == "" {
return "DOCUMENT_TYPE_UNSPECIFIED" return "DOCUMENT_TYPE_UNSPECIFIED"
} }
return label return label
} }

View File

@@ -4,7 +4,6 @@ import (
"context" "context"
"crypto/sha256" "crypto/sha256"
"encoding/hex" "encoding/hex"
"errors"
"fmt" "fmt"
"path/filepath" "path/filepath"
"strings" "strings"
@@ -41,6 +40,7 @@ func WithDiscoveryInvokeURI(uri string) Option {
if s == nil { if s == nil {
return return
} }
s.invokeURI = strings.TrimSpace(uri) s.invokeURI = strings.TrimSpace(uri)
} }
} }
@@ -51,6 +51,7 @@ func WithProducer(producer msg.Producer) Option {
if s == nil { if s == nil {
return return
} }
s.producer = producer s.producer = producer
} }
} }
@@ -61,6 +62,7 @@ func WithConfig(cfg Config) Option {
if s == nil { if s == nil {
return return
} }
s.config = cfg s.config = cfg
} }
} }
@@ -71,6 +73,7 @@ func WithDocumentStore(store docstore.Store) Option {
if s == nil { if s == nil {
return return
} }
s.docStore = store s.docStore = store
} }
} }
@@ -81,12 +84,15 @@ func WithTemplateRenderer(renderer TemplateRenderer) Option {
if s == nil { if s == nil {
return return
} }
s.template = renderer s.template = renderer
} }
} }
// Service provides billing document metadata and retrieval endpoints. // Service provides billing document metadata and retrieval endpoints.
type Service struct { type Service struct {
documentsv1.UnimplementedDocumentServiceServer
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
docStore docstore.Store docStore docstore.Store
@@ -95,12 +101,12 @@ type Service struct {
invokeURI string invokeURI string
config Config config Config
template TemplateRenderer template TemplateRenderer
documentsv1.UnimplementedDocumentServiceServer
} }
// NewService constructs a documents service with optional configuration. // NewService constructs a documents service with optional configuration.
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service { func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
initMetrics() initMetrics()
svc := &Service{ svc := &Service{
logger: logger.Named("documents"), logger: logger.Named("documents"),
storage: repo, storage: repo,
@@ -109,14 +115,17 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
for _, opt := range opts { for _, opt := range opts {
opt(svc) opt(svc)
} }
if svc.template == nil { if svc.template == nil {
if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil { if tmpl, err := newTemplateRenderer(svc.config.AcceptanceTemplatePath()); err != nil {
svc.logger.Warn("failed to load acceptance template", zap.Error(err)) svc.logger.Warn("Failed to load acceptance template", zap.Error(err))
} else { } else {
svc.template = tmpl svc.template = tmpl
} }
} }
svc.startDiscoveryAnnouncer() svc.startDiscoveryAnnouncer()
return svc return svc
} }
@@ -130,107 +139,50 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
if s.announcer != nil { if s.announcer != nil {
s.announcer.Stop() s.announcer.Stop()
} }
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: "BILLING_DOCUMENTS",
Operations: []string{"documents.batch_resolve", "documents.get"},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.BillingDocuments), announce)
s.announcer.Start()
}
func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) { func (s *Service) BatchResolveDocuments(ctx context.Context, req *documentsv1.BatchResolveDocumentsRequest) (resp *documentsv1.BatchResolveDocumentsResponse, err error) {
start := time.Now() start := time.Now()
var paymentRefs []string paymentRefs := 0
if req != nil { if req != nil {
paymentRefs = req.GetPaymentRefs() paymentRefs = len(req.GetPaymentRefs())
} }
logger := s.logger.With(zap.Int("payment_refs", len(paymentRefs)))
logger := s.logger.With(zap.Int("payment_refs", paymentRefs))
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start)) observeRequest("batch_resolve", documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED, statusLabel, time.Since(start))
observeBatchSize(len(paymentRefs)) observeBatchSize(paymentRefs)
itemsCount := 0 itemsCount := 0
if resp != nil { if resp != nil {
itemsCount = len(resp.GetItems()) itemsCount = len(resp.GetItems())
} }
fields := []zap.Field{ fields := []zap.Field{
zap.String("status", statusLabel), zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)), zap.Duration("duration", time.Since(start)),
zap.Int("items", itemsCount), zap.Int("items", itemsCount),
} }
if err != nil { if err != nil {
logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...) logger.Warn("BatchResolveDocuments failed", append(fields, zap.Error(err))...)
return return
} }
logger.Info("BatchResolveDocuments finished", fields...) logger.Info("BatchResolveDocuments finished", fields...)
}() }()
if len(paymentRefs) == 0 { _ = ctx
resp = &documentsv1.BatchResolveDocumentsResponse{} err = status.Error(codes.Unimplemented, "payment-level document flow removed; use GetOperationDocument")
return resp, nil
}
if s.storage == nil { return nil, err
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) { func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
@@ -241,6 +193,7 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
docType = req.GetType() docType = req.GetType()
paymentRef = strings.TrimSpace(req.GetPaymentRef()) paymentRef = strings.TrimSpace(req.GetPaymentRef())
} }
logger := s.logger.With( logger := s.logger.With(
zap.String("payment_ref", paymentRef), zap.String("payment_ref", paymentRef),
zap.String("document_type", docTypeLabel(docType)), zap.String("document_type", docTypeLabel(docType)),
@@ -249,6 +202,7 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
observeRequest("get_document", docType, statusLabel, time.Since(start)) observeRequest("get_document", docType, statusLabel, time.Since(start))
if resp != nil { if resp != nil {
observeDocumentBytes(docType, len(resp.GetContent())) observeDocumentBytes(docType, len(resp.GetContent()))
} }
@@ -257,93 +211,131 @@ func (s *Service) GetDocument(ctx context.Context, req *documentsv1.GetDocumentR
if resp != nil { if resp != nil {
contentBytes = len(resp.GetContent()) contentBytes = len(resp.GetContent())
} }
fields := []zap.Field{ fields := []zap.Field{
zap.String("status", statusLabel), zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)), zap.Duration("duration", time.Since(start)),
zap.Int("content_bytes", contentBytes), zap.Int("content_bytes", contentBytes),
} }
if err != nil { if err != nil {
logger.Warn("GetDocument failed", append(fields, zap.Error(err))...) logger.Warn("GetDocument failed", append(fields, zap.Error(err))...)
return return
} }
logger.Info("GetDocument finished", fields...) logger.Info("GetDocument finished", fields...)
}() }()
if paymentRef == "" { _ = ctx
err = status.Error(codes.InvalidArgument, "payment_ref is required") err = status.Error(codes.Unimplemented, "payment-level document flow removed; use GetOperationDocument")
return nil, err
} return nil, err
if docType == documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED { }
err = status.Error(codes.InvalidArgument, "document type is required")
return nil, err func (s *Service) GetOperationDocument(_ context.Context, req *documentsv1.GetOperationDocumentRequest) (resp *documentsv1.GetDocumentResponse, err error) {
} start := time.Now()
if s.storage == nil { organizationRef := ""
err = status.Error(codes.Unavailable, errStorageUnavailable.Error()) gatewayService := ""
return nil, err operationRef := ""
}
if s.docStore == nil { if req != nil {
err = status.Error(codes.Unavailable, errDocStoreUnavailable.Error()) organizationRef = strings.TrimSpace(req.GetOrganizationRef())
return nil, err gatewayService = strings.TrimSpace(req.GetGatewayService())
} operationRef = strings.TrimSpace(req.GetOperationRef())
if s.template == nil {
err = status.Error(codes.FailedPrecondition, errTemplateUnavailable.Error())
return nil, err
} }
record, err := s.storage.Documents().GetByPaymentRef(ctx, paymentRef) logger := s.logger.With(
if err != nil { zap.String("organization_ref", organizationRef),
if errors.Is(err, storage.ErrDocumentNotFound) { zap.String("gateway_service", gatewayService),
return nil, status.Error(codes.NotFound, "document record not found") zap.String("operation_ref", operationRef),
)
defer func() {
statusLabel := statusFromError(err)
docType := documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED
observeRequest("get_operation_document", docType, statusLabel, time.Since(start))
if resp != nil {
observeDocumentBytes(docType, len(resp.GetContent()))
} }
return nil, status.Error(codes.Internal, err.Error())
}
record.Normalize()
targetType := model.DocumentTypeFromProto(docType) contentBytes := 0
if resp != nil {
if docType != documentsv1.DocumentType_DOCUMENT_TYPE_ACT { contentBytes = len(resp.GetContent())
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, fields := []zap.Field{
Filename: documentFilename(docType, paymentRef), zap.String("status", statusLabel),
MimeType: "application/pdf", zap.Duration("duration", time.Since(start)),
}, nil zap.Int("content_bytes", contentBytes),
}
if err != nil {
logger.Warn("GetOperationDocument failed", append(fields, zap.Error(err))...)
return
}
logger.Info("GetOperationDocument finished", fields...)
}()
if req == nil {
err = status.Error(codes.InvalidArgument, "request is required")
return nil, err
} }
content, hash, genErr := s.generateActPDF(record.Snapshot) if organizationRef == "" {
err = status.Error(codes.InvalidArgument, "organization_ref is required")
return nil, err
}
if gatewayService == "" {
err = status.Error(codes.InvalidArgument, "gateway_service is required")
return nil, err
}
if operationRef == "" {
err = status.Error(codes.InvalidArgument, "operation_ref is required")
return nil, err
}
snapshot := operationSnapshotFromRequest(req)
content, _, genErr := s.generateOperationPDF(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())
}
path := documentStoragePath(paymentRef, docType) return nil, err
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",
} }
return resp, nil return resp, nil
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: mservice.BillingDocuments,
Operations: []string{discovery.OperationDocumentsGet},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.BillingDocuments, announce)
s.announcer.Start()
}
type serviceError string type serviceError string
func (e serviceError) Error() string { func (e serviceError) Error() string {
@@ -361,15 +353,27 @@ func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, er
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.Issuer,
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)
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
footerHash := sha256.Sum256(firstPass) footerHash := sha256.Sum256(firstPass)
footerHex := hex.EncodeToString(footerHash[:]) footerHex := hex.EncodeToString(footerHash[:])
@@ -377,22 +381,177 @@ func (s *Service) generateActPDF(snapshot model.ActSnapshot) ([]byte, string, er
if err != nil { if err != nil {
return nil, "", err return nil, "", err
} }
return finalBytes, footerHex, nil return finalBytes, footerHex, nil
} }
type operationSnapshot struct {
OrganizationRef string
GatewayService string
OperationRef string
PaymentRef string
OperationCode string
OperationLabel string
OperationState string
FailureCode string
FailureReason string
Amount string
Currency string
StartedAt time.Time
CompletedAt time.Time
}
func operationSnapshotFromRequest(req *documentsv1.GetOperationDocumentRequest) operationSnapshot {
snapshot := operationSnapshot{
OrganizationRef: strings.TrimSpace(req.GetOrganizationRef()),
GatewayService: strings.TrimSpace(req.GetGatewayService()),
OperationRef: strings.TrimSpace(req.GetOperationRef()),
PaymentRef: strings.TrimSpace(req.GetPaymentRef()),
OperationCode: strings.TrimSpace(req.GetOperationCode()),
OperationLabel: strings.TrimSpace(req.GetOperationLabel()),
OperationState: strings.TrimSpace(req.GetOperationState()),
FailureCode: strings.TrimSpace(req.GetFailureCode()),
FailureReason: strings.TrimSpace(req.GetFailureReason()),
Amount: strings.TrimSpace(req.GetAmount()),
Currency: strings.TrimSpace(req.GetCurrency()),
}
if ts := req.GetStartedAtUnixMs(); ts > 0 {
snapshot.StartedAt = time.UnixMilli(ts).UTC()
}
if ts := req.GetCompletedAtUnixMs(); ts > 0 {
snapshot.CompletedAt = time.UnixMilli(ts).UTC()
}
return snapshot
}
func buildOperationBlocks(snapshot operationSnapshot) []renderer.Block {
rows := [][]string{
{"Organization", snapshot.OrganizationRef},
{"Gateway Service", snapshot.GatewayService},
{"Operation Ref", snapshot.OperationRef},
{"Payment Ref", safeValue(snapshot.PaymentRef)},
{"Code", safeValue(snapshot.OperationCode)},
{"State", safeValue(snapshot.OperationState)},
{"Label", safeValue(snapshot.OperationLabel)},
{"Started At (UTC)", formatSnapshotTime(snapshot.StartedAt)},
{"Completed At (UTC)", formatSnapshotTime(snapshot.CompletedAt)},
}
if snapshot.Amount != "" || snapshot.Currency != "" {
rows = append(rows, []string{"Amount", strings.TrimSpace(strings.TrimSpace(snapshot.Amount) + " " + strings.TrimSpace(snapshot.Currency))})
}
blocks := []renderer.Block{
{
Tag: renderer.TagTitle,
Lines: []string{"OPERATION BILLING DOCUMENT"},
},
{
Tag: renderer.TagSubtitle,
Lines: []string{"Gateway operation statement"},
},
{
Tag: renderer.TagMeta,
Lines: []string{
"Document Type: Operation",
},
},
{
Tag: renderer.TagSection,
Lines: []string{"OPERATION DETAILS"},
},
{
Tag: renderer.TagKV,
Rows: rows,
},
}
if snapshot.FailureCode != "" || snapshot.FailureReason != "" {
blocks = append(blocks,
renderer.Block{Tag: renderer.TagSection, Lines: []string{"FAILURE DETAILS"}},
renderer.Block{
Tag: renderer.TagKV,
Rows: [][]string{
{"Failure Code", safeValue(snapshot.FailureCode)},
{"Failure Reason", safeValue(snapshot.FailureReason)},
},
},
)
}
return blocks
}
func formatSnapshotTime(value time.Time) string {
if value.IsZero() {
return "n/a"
}
return value.UTC().Format(time.RFC3339)
}
func safeValue(value string) string {
trimmed := strings.TrimSpace(value)
if trimmed == "" {
return "n/a"
}
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(), "_")
}
func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType { func toProtoTypes(types []model.DocumentType) []documentsv1.DocumentType {
if len(types) == 0 { if len(types) == 0 {
return nil return nil
} }
result := make([]documentsv1.DocumentType, 0, len(types)) result := make([]documentsv1.DocumentType, 0, len(types))
for _, t := range types { for _, t := range types {
result = append(result, t.Proto()) result = append(result, t.Proto())
} }
return result return result
} }
func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string { func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) string {
suffix := "document.pdf" suffix := "document.pdf"
switch docType { switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT: case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
suffix = "act.pdf" suffix = "act.pdf"
@@ -400,12 +559,16 @@ func documentStoragePath(paymentRef string, docType documentsv1.DocumentType) st
suffix = "invoice.pdf" suffix = "invoice.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT: case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
suffix = "receipt.pdf" suffix = "receipt.pdf"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default suffix used
} }
return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix)) return filepath.ToSlash(filepath.Join("documents", paymentRef, suffix))
} }
func documentFilename(docType documentsv1.DocumentType, paymentRef string) string { func documentFilename(docType documentsv1.DocumentType, paymentRef string) string {
name := "document" name := "document"
switch docType { switch docType {
case documentsv1.DocumentType_DOCUMENT_TYPE_ACT: case documentsv1.DocumentType_DOCUMENT_TYPE_ACT:
name = "act" name = "act"
@@ -413,6 +576,9 @@ func documentFilename(docType documentsv1.DocumentType, paymentRef string) strin
name = "invoice" name = "invoice"
case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT: case documentsv1.DocumentType_DOCUMENT_TYPE_RECEIPT:
name = "receipt" name = "receipt"
case documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED:
// default name used
} }
return fmt.Sprintf("%s_%s.pdf", name, paymentRef) return fmt.Sprintf("%s_%s.pdf", name, paymentRef)
} }

View File

@@ -12,13 +12,15 @@ import (
"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 {
store storage.DocumentsStore store storage.DocumentsStore
} }
func (s *stubRepo) Ping(ctx context.Context) error { return nil } func (s *stubRepo) Ping(_ context.Context) error { return nil }
func (s *stubRepo) Documents() storage.DocumentsStore { return s.store } func (s *stubRepo) Documents() storage.DocumentsStore { return s.store }
var _ storage.Repository = (*stubRepo)(nil) var _ storage.Repository = (*stubRepo)(nil)
@@ -28,22 +30,24 @@ type stubDocumentsStore struct {
updateCalls int updateCalls int
} }
func (s *stubDocumentsStore) Create(ctx context.Context, record *model.DocumentRecord) error { func (s *stubDocumentsStore) Create(_ context.Context, record *model.DocumentRecord) error {
s.record = record s.record = record
return nil return nil
} }
func (s *stubDocumentsStore) Update(ctx context.Context, record *model.DocumentRecord) error { func (s *stubDocumentsStore) Update(_ context.Context, record *model.DocumentRecord) error {
s.record = record s.record = record
s.updateCalls++ s.updateCalls++
return nil return nil
} }
func (s *stubDocumentsStore) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.DocumentRecord, error) { func (s *stubDocumentsStore) GetByPaymentRef(_ context.Context, _ string) (*model.DocumentRecord, error) {
return s.record, nil return s.record, nil
} }
func (s *stubDocumentsStore) ListByPaymentRefs(ctx context.Context, paymentRefs []string) ([]*model.DocumentRecord, error) { func (s *stubDocumentsStore) ListByPaymentRefs(_ context.Context, _ []string) ([]*model.DocumentRecord, error) {
return []*model.DocumentRecord{s.record}, nil return []*model.DocumentRecord{s.record}, nil
} }
@@ -59,19 +63,21 @@ func newMemDocStore() *memDocStore {
return &memDocStore{data: map[string][]byte{}} return &memDocStore{data: map[string][]byte{}}
} }
func (m *memDocStore) Save(ctx context.Context, key string, data []byte) error { func (m *memDocStore) Save(_ context.Context, key string, data []byte) error {
m.saveCount++ m.saveCount++
copyData := make([]byte, len(data)) copyData := make([]byte, len(data))
copy(copyData, data) copy(copyData, data)
m.data[key] = copyData m.data[key] = copyData
return nil return nil
} }
func (m *memDocStore) Load(ctx context.Context, key string) ([]byte, error) { func (m *memDocStore) Load(_ context.Context, key string) ([]byte, error) {
m.loadCount++ m.loadCount++
data := m.data[key] data := m.data[key]
copyData := make([]byte, len(data)) copyData := make([]byte, len(data))
copy(copyData, data) copy(copyData, data)
return copyData, nil return copyData, nil
} }
@@ -84,14 +90,13 @@ type stubTemplate struct {
calls int calls int
} }
func (s *stubTemplate) Render(snapshot model.ActSnapshot) ([]renderer.Block, error) { func (s *stubTemplate) Render(_ model.ActSnapshot) ([]renderer.Block, error) {
s.calls++ s.calls++
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),
@@ -100,14 +105,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"}},
@@ -122,74 +119,118 @@ func TestGetDocument_IdempotentAndHashed(t *testing.T) {
}, },
} }
svc := NewService(zap.NewNop(), repo, nil, svc := NewService(zap.NewNop(), nil, nil,
WithConfig(cfg), 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.Content) == 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 == "" {
if stored == "" { t.Fatalf("expected non-empty hash on first call")
t.Fatalf("expected stored hash")
} }
footerHash := extractFooterHash(resp1.Content)
footerHash := extractFooterHash(pdf1)
if footerHash == "" { if footerHash == "" {
t.Fatalf("expected footer hash in PDF") t.Fatalf("expected footer hash in PDF")
} }
if stored != footerHash {
t.Fatalf("stored hash mismatch: got %s", stored) if hash1 != footerHash {
t.Fatalf("stored hash mismatch: got %s", hash1)
} }
resp2, err := svc.GetDocument(ctx, &documentsv1.GetDocumentRequest{ 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 !bytes.Equal(resp1.Content, resp2.Content) { if hash2 == "" {
t.Fatalf("expected identical PDF bytes on second call") t.Fatalf("expected non-empty hash 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 store.saveCount != 1 { if footerHash2 != hash2 {
t.Fatalf("expected document save once, got %d", store.saveCount) t.Fatalf("second hash mismatch: got=%s want=%s", footerHash2, hash2)
}
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("Document integrity hash: ")
idx := bytes.Index(pdf, prefix) idx := bytes.Index(pdf, prefix)
if idx == -1 { if idx == -1 {
return "" return ""
} }
start := idx + len(prefix) start := idx + len(prefix)
end := start end := start
for end < len(pdf) && isHexDigit(pdf[end]) { for end < len(pdf) && isHexDigit(pdf[end]) {
end++ end++
} }
if end-start != 64 { if end-start != 64 {
return "" return ""
} }
return string(pdf[start:end]) return string(pdf[start:end])
} }
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, WithConfig(Config{
Issuer: renderer.Issuer{
LegalName: "Sendico Ltd",
},
}))
resp, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
OrganizationRef: "org-1",
GatewayService: "chain_gateway",
OperationRef: "pay-1:step-1",
PaymentRef: "pay-1",
OperationCode: "crypto.transfer",
OperationLabel: "Outbound transfer",
OperationState: "completed",
Amount: "100.50",
Currency: "USDT",
StartedAtUnixMs: time.Date(2026, 3, 4, 10, 0, 0, 0, time.UTC).UnixMilli(),
})
if err != nil {
t.Fatalf("GetOperationDocument failed: %v", err)
}
if len(resp.GetContent()) == 0 {
t.Fatalf("expected non-empty PDF content")
}
if got, want := resp.GetMimeType(), "application/pdf"; got != want {
t.Fatalf("mime_type mismatch: got=%q want=%q", got, want)
}
if got, want := resp.GetFilename(), "operation_pay-1_step-1.pdf"; got != want {
t.Fatalf("filename mismatch: got=%q want=%q", got, want)
}
}
func TestGetOperationDocument_RequiresOperationRef(t *testing.T) {
svc := NewService(zap.NewNop(), nil, nil)
_, err := svc.GetOperationDocument(context.Background(), &documentsv1.GetOperationDocumentRequest{
OrganizationRef: "org-1",
GatewayService: "chain_gateway",
})
if status.Code(err) != codes.InvalidArgument {
t.Fatalf("expected InvalidArgument, got=%v err=%v", status.Code(err), err)
}
}

View File

@@ -41,6 +41,7 @@ func (r *templateRenderer) Render(snapshot model.ActSnapshot) ([]renderer.Block,
if err := r.tpl.Execute(&buf, snapshot); err != nil { if err := r.tpl.Execute(&buf, snapshot); err != nil {
return nil, fmt.Errorf("execute template: %w", err) return nil, fmt.Errorf("execute template: %w", err)
} }
return renderer.ParseBlocks(buf.String()) return renderer.ParseBlocks(buf.String())
} }
@@ -49,6 +50,7 @@ func formatMoney(amount decimal.Decimal, currency string) string {
if currency == "" { if currency == "" {
return amount.String() return amount.String()
} }
return fmt.Sprintf("%s %s", amount.String(), currency) return fmt.Sprintf("%s %s", amount.String(), currency)
} }
@@ -56,5 +58,6 @@ func formatDate(t time.Time) string {
if t.IsZero() { if t.IsZero() {
return "" return ""
} }
return t.Format("2006-01-02") return t.Format("2006-01-02")
} }

View File

@@ -2,6 +2,7 @@ package documents
import ( import (
"path/filepath" "path/filepath"
"slices"
"testing" "testing"
"time" "time"
@@ -12,6 +13,7 @@ import (
func TestTemplateRenderer_Render(t *testing.T) { func TestTemplateRenderer_Render(t *testing.T) {
path := filepath.Join("..", "..", "..", "templates", "acceptance.tpl") path := filepath.Join("..", "..", "..", "templates", "acceptance.tpl")
tmpl, err := newTemplateRenderer(path) tmpl, err := newTemplateRenderer(path)
if err != nil { if err != nil {
t.Fatalf("newTemplateRenderer: %v", err) t.Fatalf("newTemplateRenderer: %v", err)
@@ -29,22 +31,18 @@ func TestTemplateRenderer_Render(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Render: %v", err) t.Fatalf("Render: %v", err)
} }
if len(blocks) == 0 { if len(blocks) == 0 {
t.Fatalf("expected blocks, got none") t.Fatalf("expected blocks, got none")
} }
title := findBlock(blocks, renderer.TagTitle) title := findBlock(blocks, renderer.TagTitle)
if title == nil { if title == nil {
t.Fatalf("expected title block") t.Fatalf("expected title block")
} }
foundTitle := false
for _, line := range title.Lines { if !slices.Contains(title.Lines, "ACT OF ACCEPTANCE OF SERVICES") {
if line == "ACT OF ACCEPTANCE OF SERVICES" {
foundTitle = true
break
}
}
if !foundTitle {
t.Fatalf("expected title content not found") t.Fatalf("expected title content not found")
} }
@@ -52,13 +50,17 @@ func TestTemplateRenderer_Render(t *testing.T) {
if kv == nil { if kv == nil {
t.Fatalf("expected kv block") t.Fatalf("expected kv block")
} }
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] == "Executor" && row[1] == snapshot.ExecutorFullName {
foundExecutor = true foundExecutor = true
break break
} }
} }
if !foundExecutor { if !foundExecutor {
t.Fatalf("expected executor name in kv block") t.Fatalf("expected executor name in kv block")
} }
@@ -67,13 +69,17 @@ func TestTemplateRenderer_Render(t *testing.T) {
if table == nil { if table == nil {
t.Fatalf("expected table block") t.Fatalf("expected table block")
} }
foundAmount := false foundAmount := false
for _, row := range table.Rows { for _, row := range table.Rows {
if len(row) >= 2 && row[1] == "123.45 USD" { if len(row) >= 2 && row[1] == "123.45 USD" {
foundAmount = true foundAmount = true
break break
} }
} }
if !foundAmount { if !foundAmount {
t.Fatalf("expected amount in table block") t.Fatalf("expected amount in table block")
} }
@@ -85,5 +91,6 @@ func findBlock(blocks []renderer.Block, tag renderer.Tag) *renderer.Block {
return &blocks[i] return &blocks[i]
} }
} }
return nil return nil
} }

View File

@@ -27,6 +27,7 @@ func drawHeader(pdf *gofpdf.Fpdf, issuer Issuer, marginLeft, marginTop float64)
if logoWidth > 0 { if logoWidth > 0 {
textX = startX + logoWidth + 6 textX = startX + logoWidth + 6
} }
pdf.SetXY(textX, startY) pdf.SetXY(textX, startY)
pdf.SetFont("Helvetica", "B", 12) pdf.SetFont("Helvetica", "B", 12)
pdf.CellFormat(0, 5, issuer.LegalName, "", 1, "L", false, 0, "") pdf.CellFormat(0, 5, issuer.LegalName, "", 1, "L", false, 0, "")
@@ -39,6 +40,7 @@ func drawHeader(pdf *gofpdf.Fpdf, issuer Issuer, marginLeft, marginTop float64)
} }
currentY := pdf.GetY() currentY := pdf.GetY()
if logoWidth > 0 { if logoWidth > 0 {
logoBottom := startY + logoWidth logoBottom := startY + logoWidth
if logoBottom > currentY { if logoBottom > currentY {

View File

@@ -2,7 +2,6 @@ package renderer
import ( import (
"bytes" "bytes"
"fmt"
"math" "math"
"strings" "strings"
@@ -39,18 +38,22 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
pdf.SetFooterFunc(func() { pdf.SetFooterFunc(func() {
pdf.SetY(-15) pdf.SetY(-15)
pdf.SetFont("Helvetica", "", 8) pdf.SetFont("Helvetica", "", 8)
footer := fmt.Sprintf("Document integrity hash: %s", footerHash)
footer := "Document integrity hash: " + footerHash
pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "") pdf.CellFormat(0, 5, footer, "", 0, "L", false, 0, "")
}) })
pdf.AddPage() pdf.AddPage()
if _, err := drawHeader(pdf, r.Issuer, pageMarginLeft, pageMarginTop); err != nil { if _, err := drawHeader(pdf, r.Issuer, pageMarginLeft, pageMarginTop); err != nil {
return nil, err return nil, err
} }
pdf.Ln(6) pdf.Ln(6)
for _, block := range blocks { for _, block := range blocks {
renderBlock(pdf, block) renderBlock(pdf, block)
if pdf.Error() != nil { if pdf.Error() != nil {
return nil, pdf.Error() return nil, pdf.Error()
} }
@@ -60,6 +63,7 @@ func (r Renderer) Render(blocks []Block, footerHash string) ([]byte, error) {
if err := pdf.Output(buf); err != nil { if err := pdf.Output(buf); err != nil {
return nil, err return nil, err
} }
return buf.Bytes(), nil return buf.Bytes(), nil
} }
@@ -69,47 +73,64 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
pdf.Ln(6) pdf.Ln(6)
case TagTitle: case TagTitle:
pdf.SetFont("Helvetica", "B", 14) pdf.SetFont("Helvetica", "B", 14)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(4) pdf.Ln(4)
continue continue
} }
pdf.CellFormat(0, 7, line, "", 1, "C", false, 0, "") pdf.CellFormat(0, 7, line, "", 1, "C", false, 0, "")
} }
pdf.Ln(2) pdf.Ln(2)
case TagSubtitle: case TagSubtitle:
pdf.SetFont("Helvetica", "", 11) pdf.SetFont("Helvetica", "", 11)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(3) pdf.Ln(3)
continue continue
} }
pdf.CellFormat(0, 6, line, "", 1, "C", false, 0, "") pdf.CellFormat(0, 6, line, "", 1, "C", false, 0, "")
} }
pdf.Ln(2) pdf.Ln(2)
case TagMeta: case TagMeta:
pdf.SetFont("Helvetica", "", 9) pdf.SetFont("Helvetica", "", 9)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(2) pdf.Ln(2)
continue continue
} }
pdf.CellFormat(0, 4.5, line, "", 1, "R", false, 0, "") pdf.CellFormat(0, 4.5, line, "", 1, "R", false, 0, "")
} }
pdf.Ln(2) pdf.Ln(2)
case TagSection: case TagSection:
pdf.Ln(2) pdf.Ln(2)
pdf.SetFont("Helvetica", "B", 11) pdf.SetFont("Helvetica", "B", 11)
for _, line := range block.Lines { for _, line := range block.Lines {
if strings.TrimSpace(line) == "" { if strings.TrimSpace(line) == "" {
pdf.Ln(3) pdf.Ln(3)
continue continue
} }
pdf.CellFormat(0, 6, line, "", 1, "L", false, 0, "") pdf.CellFormat(0, 6, line, "", 1, "L", false, 0, "")
} }
pdf.Ln(1) pdf.Ln(1)
case TagText: case TagText:
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
text := strings.Join(block.Lines, "\n") text := strings.Join(block.Lines, "\n")
pdf.MultiCell(0, 5, text, "", "L", false) pdf.MultiCell(0, 5, text, "", "L", false)
pdf.Ln(1) pdf.Ln(1)
@@ -119,12 +140,14 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
renderTable(pdf, block) renderTable(pdf, block)
case TagSign: case TagSign:
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
text := strings.Join(block.Lines, "\n") text := strings.Join(block.Lines, "\n")
pdf.MultiCell(0, 6, text, "", "L", false) pdf.MultiCell(0, 6, text, "", "L", false)
pdf.Ln(2) pdf.Ln(2)
default: default:
// Unknown tag: treat as plain text for resilience. // Unknown tag: treat as plain text for resilience.
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
text := strings.Join(block.Lines, "\n") text := strings.Join(block.Lines, "\n")
pdf.MultiCell(0, 5, text, "", "L", false) pdf.MultiCell(0, 5, text, "", "L", false)
pdf.Ln(1) pdf.Ln(1)
@@ -133,6 +156,7 @@ func renderBlock(pdf *gofpdf.Fpdf, block Block) {
func renderKeyValue(pdf *gofpdf.Fpdf, block Block) { func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
usable := usableWidth(pdf) usable := usableWidth(pdf)
keyWidth := math.Round(usable * 0.35) keyWidth := math.Round(usable * 0.35)
valueWidth := usable - keyWidth valueWidth := usable - keyWidth
@@ -142,11 +166,14 @@ func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
if len(row) == 0 { if len(row) == 0 {
continue continue
} }
key := row[0] key := row[0]
value := "" value := ""
if len(row) > 1 { if len(row) > 1 {
value = row[1] value = row[1]
} }
x := pdf.GetX() x := pdf.GetX()
y := pdf.GetY() y := pdf.GetY()
@@ -162,6 +189,7 @@ func renderKeyValue(pdf *gofpdf.Fpdf, block Block) {
pdf.SetY(maxFloat(leftY, rightY)) pdf.SetY(maxFloat(leftY, rightY))
} }
pdf.Ln(1) pdf.Ln(1)
} }
@@ -169,6 +197,7 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
if len(block.Rows) == 0 { if len(block.Rows) == 0 {
return return
} }
usable := usableWidth(pdf) usable := usableWidth(pdf)
col1 := math.Round(usable * 0.7) col1 := math.Round(usable * 0.7)
col2 := usable - col1 col2 := usable - col1
@@ -176,9 +205,11 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
header := block.Rows[0] header := block.Rows[0]
pdf.SetFont("Helvetica", "B", 10) pdf.SetFont("Helvetica", "B", 10)
if len(header) > 0 { if len(header) > 0 {
pdf.CellFormat(col1, lineHeight, header[0], "1", 0, "L", false, 0, "") pdf.CellFormat(col1, lineHeight, header[0], "1", 0, "L", false, 0, "")
} }
if len(header) > 1 { if len(header) > 1 {
pdf.CellFormat(col2, lineHeight, header[1], "1", 1, "R", false, 0, "") pdf.CellFormat(col2, lineHeight, header[1], "1", 1, "R", false, 0, "")
} else { } else {
@@ -186,15 +217,19 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
} }
pdf.SetFont("Helvetica", "", 10) pdf.SetFont("Helvetica", "", 10)
for _, row := range block.Rows[1:] { for _, row := range block.Rows[1:] {
colA := "" colA := ""
colB := "" colB := ""
if len(row) > 0 { if len(row) > 0 {
colA = row[0] colA = row[0]
} }
if len(row) > 1 { if len(row) > 1 {
colB = row[1] colB = row[1]
} }
x := pdf.GetX() x := pdf.GetX()
y := pdf.GetY() y := pdf.GetY()
pdf.MultiCell(col1, lineHeight, colA, "1", "L", false) pdf.MultiCell(col1, lineHeight, colA, "1", "L", false)
@@ -204,12 +239,14 @@ func renderTable(pdf *gofpdf.Fpdf, block Block) {
rightY := pdf.GetY() rightY := pdf.GetY()
pdf.SetY(maxFloat(leftY, rightY)) pdf.SetY(maxFloat(leftY, rightY))
} }
pdf.Ln(2) pdf.Ln(2)
} }
func usableWidth(pdf *gofpdf.Fpdf) float64 { func usableWidth(pdf *gofpdf.Fpdf) float64 {
pageW, _ := pdf.GetPageSize() pageW, _ := pdf.GetPageSize()
left, _, right, _ := pdf.GetMargins() left, _, right, _ := pdf.GetMargins()
return pageW - left - right return pageW - left - right
} }
@@ -217,5 +254,6 @@ func maxFloat(a, b float64) float64 {
if a > b { if a > b {
return a return a
} }
return b return b
} }

View File

@@ -26,11 +26,13 @@ func TestRenderer_RenderContainsText(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("Render: %v", err) t.Fatalf("Render: %v", err)
} }
if len(pdfBytes) == 0 { if len(pdfBytes) == 0 {
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", "Document integrity hash"}
for _, token := range checks { for _, token := range checks {
if !containsPDFText(pdfBytes, token) { if !containsPDFText(pdfBytes, token) {
t.Fatalf("expected PDF to contain %q", token) t.Fatalf("expected PDF to contain %q", token)
@@ -42,22 +44,29 @@ func containsPDFText(pdfBytes []byte, text string) bool {
if bytes.Contains(pdfBytes, []byte(text)) { if bytes.Contains(pdfBytes, []byte(text)) {
return true return true
} }
hexText := hex.EncodeToString([]byte(text)) hexText := hex.EncodeToString([]byte(text))
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(hexText))) { if bytes.Contains(pdfBytes, []byte(strings.ToUpper(hexText))) {
return true return true
} }
if bytes.Contains(pdfBytes, []byte(strings.ToLower(hexText))) { if bytes.Contains(pdfBytes, []byte(strings.ToLower(hexText))) {
return true return true
} }
utf16Bytes := encodeUTF16BE(text, false) utf16Bytes := encodeUTF16BE(text, false)
if bytes.Contains(pdfBytes, utf16Bytes) { if bytes.Contains(pdfBytes, utf16Bytes) {
return true return true
} }
utf16Hex := hex.EncodeToString(utf16Bytes) utf16Hex := hex.EncodeToString(utf16Bytes)
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16Hex))) { if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16Hex))) {
return true return true
} }
if bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16Hex))) { if bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16Hex))) {
return true return true
} }
@@ -66,25 +75,33 @@ func containsPDFText(pdfBytes []byte, text string) bool {
if bytes.Contains(pdfBytes, utf16BytesBOM) { if bytes.Contains(pdfBytes, utf16BytesBOM) {
return true return true
} }
utf16HexBOM := hex.EncodeToString(utf16BytesBOM) utf16HexBOM := hex.EncodeToString(utf16BytesBOM)
if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16HexBOM))) { if bytes.Contains(pdfBytes, []byte(strings.ToUpper(utf16HexBOM))) {
return true return true
} }
return bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16HexBOM))) return bytes.Contains(pdfBytes, []byte(strings.ToLower(utf16HexBOM)))
} }
func encodeUTF16BE(text string, withBOM bool) []byte { func encodeUTF16BE(text string, withBOM bool) []byte {
encoded := utf16.Encode([]rune(text)) encoded := utf16.Encode([]rune(text))
length := len(encoded) * 2 length := len(encoded) * 2
if withBOM { if withBOM {
length += 2 length += 2
} }
out := make([]byte, 0, length) out := make([]byte, 0, length)
if withBOM { if withBOM {
out = append(out, 0xFE, 0xFF) out = append(out, 0xFE, 0xFF)
} }
for _, v := range encoded { for _, v := range encoded {
out = append(out, byte(v>>8), byte(v)) out = append(out, byte(v>>8), byte(v))
} }
return out return out
} }

View File

@@ -32,6 +32,7 @@ type Block struct {
func ParseBlocks(input string) ([]Block, error) { func ParseBlocks(input string) ([]Block, error) {
scanner := bufio.NewScanner(strings.NewReader(input)) scanner := bufio.NewScanner(strings.NewReader(input))
blocks := make([]Block, 0) blocks := make([]Block, 0)
var current *Block var current *Block
flush := func() { flush := func() {
@@ -44,17 +45,24 @@ func ParseBlocks(input string) ([]Block, error) {
for scanner.Scan() { for scanner.Scan() {
line := strings.TrimRight(scanner.Text(), "\r") line := strings.TrimRight(scanner.Text(), "\r")
trimmed := strings.TrimSpace(line) trimmed := strings.TrimSpace(line)
if strings.HasPrefix(trimmed, "#") { if strings.HasPrefix(trimmed, "#") {
flush() flush()
tag := Tag(strings.TrimSpace(strings.TrimPrefix(trimmed, "#"))) tag := Tag(strings.TrimSpace(strings.TrimPrefix(trimmed, "#")))
if tag == "" { if tag == "" {
continue continue
} }
if tag == TagSpacer { if tag == TagSpacer {
blocks = append(blocks, Block{Tag: TagSpacer}) blocks = append(blocks, Block{Tag: TagSpacer})
continue continue
} }
current = &Block{Tag: tag} current = &Block{Tag: tag}
continue continue
} }
@@ -62,16 +70,19 @@ func ParseBlocks(input string) ([]Block, error) {
continue continue
} }
switch current.Tag { switch current.Tag { //nolint:exhaustive // only KV and Table need row parsing
case TagKV, TagTable: case TagKV, TagTable:
if trimmed == "" { if trimmed == "" {
continue continue
} }
parts := strings.Split(line, "|") parts := strings.Split(line, "|")
row := make([]string, 0, len(parts)) row := make([]string, 0, len(parts))
for _, part := range parts { for _, part := range parts {
row = append(row, strings.TrimSpace(part)) row = append(row, strings.TrimSpace(part))
} }
current.Rows = append(current.Rows, row) current.Rows = append(current.Rows, row)
default: default:
current.Lines = append(current.Lines, line) current.Lines = append(current.Lines, line)
@@ -83,5 +94,6 @@ func ParseBlocks(input string) ([]Block, error) {
} }
flush() flush()
return blocks, nil return blocks, nil
} }

View File

@@ -28,6 +28,7 @@ func DocumentTypeFromProto(t documentsv1.DocumentType) DocumentType {
if name, ok := documentsv1.DocumentType_name[int32(t)]; ok { if name, ok := documentsv1.DocumentType_name[int32(t)]; ok {
return DocumentType(name) return DocumentType(name)
} }
return DocumentTypeUnspecified return DocumentTypeUnspecified
} }
@@ -36,22 +37,24 @@ func (t DocumentType) Proto() documentsv1.DocumentType {
if value, ok := documentsv1.DocumentType_value[string(t)]; ok { if value, ok := documentsv1.DocumentType_value[string(t)]; ok {
return documentsv1.DocumentType(value) return documentsv1.DocumentType(value)
} }
return documentsv1.DocumentType_DOCUMENT_TYPE_UNSPECIFIED 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"`
Date time.Time `bson:"date" json:"date"` Date time.Time `bson:"date" json:"date"`
ExecutorFullName string `bson:"executorFullName" json:"executorFullName"` ExecutorFullName string `bson:"executorFullName" json:"executorFullName"`
Amount decimal.Decimal `bson:"amount" json:"amount"` Amount decimal.Decimal `bson:"amount" json:"amount"`
Currency string `bson:"currency" json:"currency"` Currency string `bson:"currency" json:"currency"`
} }
func (s *ActSnapshot) Normalize() { func (s *ActSnapshot) Normalize() {
if s == nil { if s == nil {
return return
} }
s.PaymentID = strings.TrimSpace(s.PaymentID) s.PaymentID = strings.TrimSpace(s.PaymentID)
s.ExecutorFullName = strings.TrimSpace(s.ExecutorFullName) s.ExecutorFullName = strings.TrimSpace(s.ExecutorFullName)
s.Currency = strings.TrimSpace(s.Currency) s.Currency = strings.TrimSpace(s.Currency)
@@ -60,21 +63,25 @@ func (s *ActSnapshot) Normalize() {
// DocumentRecord stores document metadata and cached artefacts for a payment. // DocumentRecord stores document metadata and cached artefacts for a payment.
type DocumentRecord struct { type DocumentRecord struct {
storable.Base `bson:",inline" json:",inline"` storable.Base `bson:",inline" json:",inline"`
PaymentRef string `bson:"paymentRef" json:"paymentRef"`
Snapshot ActSnapshot `bson:"snapshot" json:"snapshot"` PaymentRef string `bson:"paymentRef" json:"paymentRef"`
StoragePaths map[DocumentType]string `bson:"storagePaths,omitempty" json:"storagePaths,omitempty"` Snapshot ActSnapshot `bson:"snapshot" json:"snapshot"`
Hashes map[DocumentType]string `bson:"hashes,omitempty" json:"hashes,omitempty"` StoragePaths map[DocumentType]string `bson:"storagePaths,omitempty" json:"storagePaths,omitempty"`
Hashes map[DocumentType]string `bson:"hashes,omitempty" json:"hashes,omitempty"`
} }
func (r *DocumentRecord) Normalize() { func (r *DocumentRecord) Normalize() {
if r == nil { if r == nil {
return return
} }
r.PaymentRef = strings.TrimSpace(r.PaymentRef) r.PaymentRef = strings.TrimSpace(r.PaymentRef)
r.Snapshot.Normalize() r.Snapshot.Normalize()
if r.StoragePaths == nil { if r.StoragePaths == nil {
r.StoragePaths = map[DocumentType]string{} r.StoragePaths = map[DocumentType]string{}
} }
if r.Hashes == nil { if r.Hashes == nil {
r.Hashes = map[DocumentType]string{} r.Hashes = map[DocumentType]string{}
} }

View File

@@ -42,18 +42,22 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
defer cancel() defer cancel()
if err := result.Ping(ctx); err != nil { if err := result.Ping(ctx); err != nil {
result.logger.Error("mongo ping failed during store init", zap.Error(err)) result.logger.Error("Mongo ping failed during store init", zap.Error(err))
return nil, err return nil, err
} }
documentsStore, err := store.NewDocuments(result.logger, database) documentsStore, err := store.NewDocuments(result.logger, database)
if err != nil { if err != nil {
result.logger.Error("failed to initialise documents store", zap.Error(err)) result.logger.Error("Failed to initialise documents store", zap.Error(err))
return nil, err return nil, err
} }
result.documents = documentsStore result.documents = documentsStore
result.logger.Info("Billing documents MongoDB storage initialised") result.logger.Info("Billing documents MongoDB storage initialised")
return result, nil return result, nil
} }

View File

@@ -38,13 +38,14 @@ func NewDocuments(logger mlogger.Logger, db *mongo.Database) (*Documents, error)
for _, def := range indexes { for _, def := range indexes {
if err := repo.CreateIndex(def); err != nil { if err := repo.CreateIndex(def); err != nil {
logger.Error("failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection())) logger.Error("Failed to ensure documents index", zap.Error(err), zap.String("collection", repo.Collection()))
return nil, err return nil, err
} }
} }
childLogger := logger.Named("documents") childLogger := logger.Named("documents")
childLogger.Debug("documents store initialised") childLogger.Debug("Documents store initialised")
return &Documents{ return &Documents{
logger: childLogger, logger: childLogger,
@@ -56,7 +57,9 @@ func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) er
if record == nil { if record == nil {
return merrors.InvalidArgument("documentsStore: nil record") return merrors.InvalidArgument("documentsStore: nil record")
} }
record.Normalize() record.Normalize()
if record.PaymentRef == "" { if record.PaymentRef == "" {
return merrors.InvalidArgument("documentsStore: empty paymentRef") return merrors.InvalidArgument("documentsStore: empty paymentRef")
} }
@@ -66,9 +69,12 @@ func (d *Documents) Create(ctx context.Context, record *model.DocumentRecord) er
if errors.Is(err, merrors.ErrDataConflict) { if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateDocument return storage.ErrDuplicateDocument
} }
return err return err
} }
d.logger.Debug("document record created", zap.String("payment_ref", record.PaymentRef))
d.logger.Debug("Document record created", zap.String("payment_ref", record.PaymentRef))
return nil return nil
} }
@@ -76,17 +82,21 @@ func (d *Documents) Update(ctx context.Context, record *model.DocumentRecord) er
if record == nil { if record == nil {
return merrors.InvalidArgument("documentsStore: nil record") return merrors.InvalidArgument("documentsStore: nil record")
} }
if record.ID.IsZero() { if record.ID.IsZero() {
return merrors.InvalidArgument("documentsStore: missing record id") return merrors.InvalidArgument("documentsStore: missing record id")
} }
record.Normalize() record.Normalize()
record.Update() record.Update()
if err := d.repo.Update(ctx, record); err != nil { if err := d.repo.Update(ctx, record); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return storage.ErrDocumentNotFound return storage.ErrDocumentNotFound
} }
return err return err
} }
return nil return nil
} }
@@ -101,8 +111,10 @@ func (d *Documents) GetByPaymentRef(ctx context.Context, paymentRef string) (*mo
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrDocumentNotFound return nil, storage.ErrDocumentNotFound
} }
return nil, err return nil, err
} }
return entity, nil return entity, nil
} }
@@ -113,26 +125,34 @@ func (d *Documents) ListByPaymentRefs(ctx context.Context, paymentRefs []string)
if clean == "" { if clean == "" {
continue continue
} }
refs = append(refs, clean) refs = append(refs, clean)
} }
if len(refs) == 0 { if len(refs) == 0 {
return []*model.DocumentRecord{}, nil return []*model.DocumentRecord{}, nil
} }
query := repository.Query().Comparison(repository.Field("paymentRef"), builder.In, refs) query := repository.Query().Comparison(repository.Field("paymentRef"), builder.In, refs)
records := make([]*model.DocumentRecord, 0) records := make([]*model.DocumentRecord, 0)
decoder := func(cur *mongo.Cursor) error { decoder := func(cur *mongo.Cursor) error {
var rec model.DocumentRecord var rec model.DocumentRecord
if err := cur.Decode(&rec); err != nil { if err := cur.Decode(&rec); err != nil {
d.logger.Warn("failed to decode document record", zap.Error(err)) d.logger.Warn("Failed to decode document record", zap.Error(err))
return err return err
} }
records = append(records, &rec) records = append(records, &rec)
return nil return nil
} }
if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil { if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil {
return nil, err return nil, err
} }
return records, nil return records, nil
} }

View File

@@ -0,0 +1,198 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# 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:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- noinlineerr
- wsl
- wrapcheck
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# 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

@@ -1,6 +1,6 @@
module github.com/tech/sendico/billing/fees module github.com/tech/sendico/billing/fees
go 1.25.6 go 1.25.7
replace github.com/tech/sendico/pkg => ../../pkg replace github.com/tech/sendico/pkg => ../../pkg
@@ -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.78.0 google.golang.org/grpc v1.79.1
gopkg.in/yaml.v3 v3.0.1 gopkg.in/yaml.v3 v3.0.1
) )
@@ -25,31 +25,31 @@ require (
github.com/casbin/casbin/v2 v2.135.0 // indirect github.com/casbin/casbin/v2 v2.135.0 // indirect
github.com/casbin/govaluate v1.10.0 // indirect github.com/casbin/govaluate v1.10.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/go-chi/chi/v5 v5.2.4 // indirect github.com/go-chi/chi/v5 v5.2.5 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
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.3 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
) )

View File

@@ -38,8 +38,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -152,16 +152,16 @@ 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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -191,16 +191,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/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

@@ -24,5 +24,6 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&info) return vf.Create(&info)
} }

View File

@@ -31,7 +31,8 @@ type Imp struct {
type config struct { type config struct {
*grpcapp.Config `yaml:",inline"` *grpcapp.Config `yaml:",inline"`
Oracle OracleConfig `yaml:"oracle"`
Oracle OracleConfig `yaml:"oracle"`
} }
type OracleConfig struct { type OracleConfig struct {
@@ -45,6 +46,7 @@ func (c OracleConfig) dialTimeout() time.Duration {
if c.DialTimeoutSecs <= 0 { if c.DialTimeoutSecs <= 0 {
return 5 * time.Second return 5 * time.Second
} }
return time.Duration(c.DialTimeoutSecs) * time.Second return time.Duration(c.DialTimeoutSecs) * time.Second
} }
@@ -52,6 +54,7 @@ func (c OracleConfig) callTimeout() time.Duration {
if c.CallTimeoutSecs <= 0 { if c.CallTimeoutSecs <= 0 {
return 3 * time.Second return 3 * time.Second
} }
return time.Duration(c.CallTimeoutSecs) * time.Second return time.Duration(c.CallTimeoutSecs) * time.Second
} }
@@ -69,9 +72,11 @@ func (i *Imp) Shutdown() {
if i.service != nil { if i.service != nil {
i.service.Shutdown() i.service.Shutdown()
} }
if i.oracleClient != nil { if i.oracleClient != nil {
_ = i.oracleClient.Close() _ = i.oracleClient.Close()
} }
return return
} }
@@ -98,6 +103,7 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) {
@@ -105,22 +111,23 @@ func (i *Imp) Start() error {
} }
var oracleClient oracleclient.Client var oracleClient oracleclient.Client
if addr := strings.TrimSpace(cfg.Oracle.Address); addr != "" { if addr := strings.TrimSpace(cfg.Oracle.Address); addr != "" {
dialCtx, cancel := context.WithTimeout(context.Background(), cfg.Oracle.dialTimeout()) dialCtx, cancel := context.WithTimeout(context.Background(), cfg.Oracle.dialTimeout())
defer cancel() defer cancel()
oc, err := oracleclient.New(dialCtx, oracleclient.Config{ oracleConn, err := oracleclient.New(dialCtx, oracleclient.Config{
Address: addr, Address: addr,
DialTimeout: cfg.Oracle.dialTimeout(), DialTimeout: cfg.Oracle.dialTimeout(),
CallTimeout: cfg.Oracle.callTimeout(), CallTimeout: cfg.Oracle.callTimeout(),
Insecure: cfg.Oracle.InsecureTransport, Insecure: cfg.Oracle.InsecureTransport,
}) })
if err != nil { if err != nil {
i.logger.Warn("failed to initialise oracle client", zap.String("address", addr), zap.Error(err)) i.logger.Warn("Failed to initialise oracle client", zap.String("address", addr), zap.Error(err))
} else { } else {
oracleClient = oc oracleClient = oracleConn
i.oracleClient = oc i.oracleClient = oracleConn
i.logger.Info("connected to oracle service", zap.String("address", addr)) i.logger.Info("Connected to oracle service", zap.String("address", addr))
} }
} }
@@ -129,13 +136,16 @@ func (i *Imp) Start() error {
if oracleClient != nil { if oracleClient != nil {
opts = append(opts, fees.WithOracleClient(oracleClient)) opts = append(opts, fees.WithOracleClient(oracleClient))
} }
if cfg.GRPC != nil { if cfg.GRPC != nil {
if invokeURI := cfg.GRPC.DiscoveryInvokeURI(); invokeURI != "" { if invokeURI := cfg.GRPC.DiscoveryInvokeURI(); invokeURI != "" {
opts = append(opts, fees.WithDiscoveryInvokeURI(invokeURI)) opts = append(opts, fees.WithDiscoveryInvokeURI(invokeURI))
} }
} }
svc := fees.NewService(logger, repo, producer, opts...) svc := fees.NewService(logger, repo, producer, opts...)
i.service = svc i.service = svc
return svc, nil return svc, nil
} }
@@ -143,6 +153,7 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.app = app i.app = app
return i.app.Start() return i.app.Start()
@@ -152,12 +163,14 @@ func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err return nil, err
} }
cfg := &config{Config: &grpcapp.Config{}} cfg := &config{Config: &grpcapp.Config{}}
if err := yaml.Unmarshal(data, cfg); err != nil { if err := yaml.Unmarshal(data, cfg); err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err return nil, err
} }

View File

@@ -3,6 +3,7 @@ package calculator
import ( import (
"context" "context"
"errors" "errors"
"maps"
"math/big" "math/big"
"sort" "sort"
"strconv" "strconv"
@@ -33,6 +34,7 @@ func New(logger mlogger.Logger, oracle fxOracle) *quoteCalculator {
if logger == nil { if logger == nil {
logger = zap.NewNop() logger = zap.NewNop()
} }
return &quoteCalculator{ return &quoteCalculator{
logger: logger.Named("calculator"), logger: logger.Named("calculator"),
oracle: oracle, oracle: oracle,
@@ -45,27 +47,10 @@ type quoteCalculator struct {
} }
func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, intent *feesv1.Intent, bookedAt time.Time, _ *tracev1.TraceContext) (*types.CalculationResult, error) { func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, intent *feesv1.Intent, bookedAt time.Time, _ *tracev1.TraceContext) (*types.CalculationResult, error) {
if plan == nil { baseAmount, baseScale, trigger, err := validateComputeInputs(plan, intent)
return nil, merrors.InvalidArgument("plan is required")
}
if intent == nil {
return nil, merrors.InvalidArgument("intent is required")
}
trigger := convertTrigger(intent.GetTrigger())
if trigger == model.TriggerUnspecified {
return nil, merrors.InvalidArgument("unsupported trigger")
}
baseAmount, err := dmath.RatFromString(intent.GetBaseAmount().GetAmount())
if err != nil { if err != nil {
return nil, merrors.InvalidArgument("invalid base amount") return nil, err
} }
if baseAmount.Sign() < 0 {
return nil, merrors.InvalidArgument("base amount cannot be negative")
}
baseScale := inferScale(intent.GetBaseAmount().GetAmount())
rules := make([]model.FeeRule, len(plan.Rules)) rules := make([]model.FeeRule, len(plan.Rules))
copy(rules, plan.Rules) copy(rules, plan.Rules)
@@ -73,81 +58,37 @@ func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, inte
if rules[i].Priority == rules[j].Priority { if rules[i].Priority == rules[j].Priority {
return rules[i].RuleID < rules[j].RuleID return rules[i].RuleID < rules[j].RuleID
} }
return rules[i].Priority < rules[j].Priority return rules[i].Priority < rules[j].Priority
}) })
planID := planIDFrom(plan)
lines := make([]*feesv1.DerivedPostingLine, 0, len(rules)) lines := make([]*feesv1.DerivedPostingLine, 0, len(rules))
applied := make([]*feesv1.AppliedRule, 0, len(rules)) applied := make([]*feesv1.AppliedRule, 0, len(rules))
planID := ""
if planRef := plan.GetID(); planRef != nil && !planRef.IsZero() {
planID = planRef.Hex()
}
for _, rule := range rules { for _, rule := range rules {
if !shouldApplyRule(rule, trigger, intent.GetAttributes(), bookedAt) { if !shouldApplyRule(rule, trigger, intent.GetAttributes(), bookedAt) {
continue continue
} }
ledgerAccountRef := strings.TrimSpace(rule.LedgerAccountRef)
amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule) amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule)
if calcErr != nil { if calcErr != nil {
if !errors.Is(calcErr, merrors.ErrInvalidArg) { if !errors.Is(calcErr, merrors.ErrInvalidArg) {
c.logger.Warn("failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr)) c.logger.Warn("Failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr))
} }
continue continue
} }
if amount.Sign() == 0 { if amount.Sign() == 0 {
continue continue
} }
currency := intent.GetBaseAmount().GetCurrency() currency := resolvedCurrency(intent.GetBaseAmount().GetCurrency(), rule.Currency)
if override := strings.TrimSpace(rule.Currency); override != "" { entrySide := resolvedEntrySide(rule)
currency = override
}
entrySide := mapEntrySide(rule.EntrySide) lines = append(lines, buildPostingLine(rule, amount, scale, currency, entrySide, planID))
if entrySide == accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED { applied = append(applied, buildAppliedRule(rule, planID))
// Default fees to debit (i.e. charge the customer) when entry side is not specified.
entrySide = accountingv1.EntrySide_ENTRY_SIDE_DEBIT
}
meta := map[string]string{
"fee_rule_id": rule.RuleID,
}
if planID != "" {
meta["fee_plan_id"] = planID
}
if rule.Metadata != nil {
if taxCode := strings.TrimSpace(rule.Metadata["tax_code"]); taxCode != "" {
meta["tax_code"] = taxCode
}
if taxRate := strings.TrimSpace(rule.Metadata["tax_rate"]); taxRate != "" {
meta["tax_rate"] = taxRate
}
}
lines = append(lines, &feesv1.DerivedPostingLine{
LedgerAccountRef: ledgerAccountRef,
Money: &moneyv1.Money{
Amount: dmath.FormatRat(amount, scale),
Currency: currency,
},
LineType: mapLineType(rule.LineType),
Side: entrySide,
Meta: meta,
})
applied = append(applied, &feesv1.AppliedRule{
RuleId: rule.RuleID,
RuleVersion: planID,
Formula: rule.Formula,
Rounding: mapRoundingMode(rule.Rounding),
TaxCode: metadataValue(rule.Metadata, "tax_code"),
TaxRate: metadataValue(rule.Metadata, "tax_rate"),
Parameters: cloneStringMap(rule.Metadata),
})
} }
var fxUsed *feesv1.FXUsed var fxUsed *feesv1.FXUsed
@@ -170,40 +111,24 @@ func (c *quoteCalculator) calculateRuleAmount(baseAmount *big.Rat, baseScale uin
result := new(big.Rat) result := new(big.Rat)
if percentage := strings.TrimSpace(rule.Percentage); percentage != "" { result, err = applyPercentage(result, baseAmount, rule.Percentage)
percentageRat, perr := dmath.RatFromString(percentage) if err != nil {
if perr != nil { return nil, 0, err
return nil, 0, merrors.InvalidArgument("invalid percentage")
}
result = dmath.AddRat(result, dmath.MulRat(baseAmount, percentageRat))
} }
if fixed := strings.TrimSpace(rule.FixedAmount); fixed != "" { result, err = applyFixed(result, rule.FixedAmount)
fixedRat, ferr := dmath.RatFromString(fixed) if err != nil {
if ferr != nil { return nil, 0, err
return nil, 0, merrors.InvalidArgument("invalid fixed amount")
}
result = dmath.AddRat(result, fixedRat)
} }
if minStr := strings.TrimSpace(rule.MinimumAmount); minStr != "" { result, err = applyMin(result, rule.MinimumAmount)
minRat, merr := dmath.RatFromString(minStr) if err != nil {
if merr != nil { return nil, 0, err
return nil, 0, merrors.InvalidArgument("invalid minimum amount")
}
if dmath.CmpRat(result, minRat) < 0 {
result = new(big.Rat).Set(minRat)
}
} }
if maxStr := strings.TrimSpace(rule.MaximumAmount); maxStr != "" { result, err = applyMax(result, rule.MaximumAmount)
maxRat, merr := dmath.RatFromString(maxStr) if err != nil {
if merr != nil { return nil, 0, err
return nil, 0, merrors.InvalidArgument("invalid maximum amount")
}
if dmath.CmpRat(result, maxRat) > 0 {
result = new(big.Rat).Set(maxRat)
}
} }
if result.Sign() < 0 { if result.Sign() < 0 {
@@ -218,6 +143,66 @@ func (c *quoteCalculator) calculateRuleAmount(baseAmount *big.Rat, baseScale uin
return rounded, scale, nil return rounded, scale, nil
} }
func applyPercentage(result, baseAmount *big.Rat, percentage string) (*big.Rat, error) {
if strings.TrimSpace(percentage) == "" {
return result, nil
}
percentageRat, err := dmath.RatFromString(percentage)
if err != nil {
return nil, merrors.InvalidArgument("invalid percentage")
}
return dmath.AddRat(result, dmath.MulRat(baseAmount, percentageRat)), nil
}
func applyFixed(result *big.Rat, fixed string) (*big.Rat, error) {
if strings.TrimSpace(fixed) == "" {
return result, nil
}
fixedRat, err := dmath.RatFromString(fixed)
if err != nil {
return nil, merrors.InvalidArgument("invalid fixed amount")
}
return dmath.AddRat(result, fixedRat), nil
}
func applyMin(result *big.Rat, minStr string) (*big.Rat, error) {
if strings.TrimSpace(minStr) == "" {
return result, nil
}
minRat, err := dmath.RatFromString(minStr)
if err != nil {
return nil, merrors.InvalidArgument("invalid minimum amount")
}
if dmath.CmpRat(result, minRat) < 0 {
return new(big.Rat).Set(minRat), nil
}
return result, nil
}
func applyMax(result *big.Rat, maxStr string) (*big.Rat, error) {
if strings.TrimSpace(maxStr) == "" {
return result, nil
}
maxRat, err := dmath.RatFromString(maxStr)
if err != nil {
return nil, merrors.InvalidArgument("invalid maximum amount")
}
if dmath.CmpRat(result, maxRat) > 0 {
return new(big.Rat).Set(maxRat), nil
}
return result, nil
}
const ( const (
attrFxBaseCurrency = "fx_base_currency" attrFxBaseCurrency = "fx_base_currency"
attrFxQuoteCurrency = "fx_quote_currency" attrFxQuoteCurrency = "fx_quote_currency"
@@ -232,7 +217,9 @@ func (c *quoteCalculator) buildFxUsed(ctx context.Context, intent *feesv1.Intent
} }
attrs := intent.GetAttributes() attrs := intent.GetAttributes()
base := strings.TrimSpace(attrs[attrFxBaseCurrency]) base := strings.TrimSpace(attrs[attrFxBaseCurrency])
quote := strings.TrimSpace(attrs[attrFxQuoteCurrency]) quote := strings.TrimSpace(attrs[attrFxQuoteCurrency])
if base == "" || quote == "" { if base == "" || quote == "" {
return nil return nil
@@ -247,20 +234,26 @@ func (c *quoteCalculator) buildFxUsed(ctx context.Context, intent *feesv1.Intent
Provider: provider, Provider: provider,
}) })
if err != nil { if err != nil {
c.logger.Warn("fees: failed to fetch FX context", zap.Error(err)) c.logger.Warn("Fees: failed to fetch FX context", zap.Error(err))
return nil return nil
} }
if snapshot == nil { if snapshot == nil {
return nil return nil
} }
rateValue := strings.TrimSpace(attrs[attrFxRateOverride]) rateValue := strings.TrimSpace(attrs[attrFxRateOverride])
if rateValue == "" { if rateValue == "" {
rateValue = snapshot.Mid rateValue = snapshot.Mid
} }
if rateValue == "" { if rateValue == "" {
rateValue = snapshot.Ask rateValue = snapshot.Ask
} }
if rateValue == "" { if rateValue == "" {
rateValue = snapshot.Bid rateValue = snapshot.Bid
} }
@@ -292,15 +285,19 @@ func inferScale(amount string) uint32 {
if value == "" { if value == "" {
return 0 return 0
} }
if idx := strings.IndexAny(value, "eE"); idx >= 0 { if idx := strings.IndexAny(value, "eE"); idx >= 0 {
value = value[:idx] value = value[:idx]
} }
if strings.HasPrefix(value, "+") || strings.HasPrefix(value, "-") { if strings.HasPrefix(value, "+") || strings.HasPrefix(value, "-") {
value = value[1:] value = value[1:]
} }
if dot := strings.IndexByte(value, '.'); dot >= 0 {
return uint32(len(value[dot+1:])) if _, after, found := strings.Cut(value, "."); found {
return uint32(len(after)) //nolint:gosec // decimal scale; cannot overflow
} }
return 0 return 0
} }
@@ -308,12 +305,15 @@ func shouldApplyRule(rule model.FeeRule, trigger model.Trigger, attributes map[s
if rule.Trigger != trigger { if rule.Trigger != trigger {
return false return false
} }
if rule.EffectiveFrom.After(bookedAt) { if rule.EffectiveFrom.After(bookedAt) {
return false return false
} }
if rule.EffectiveTo != nil && rule.EffectiveTo.Before(bookedAt) { if rule.EffectiveTo != nil && rule.EffectiveTo.Before(bookedAt) {
return false return false
} }
return ruleMatchesAttributes(rule, attributes) return ruleMatchesAttributes(rule, attributes)
} }
@@ -325,6 +325,7 @@ func resolveRuleScale(rule model.FeeRule, fallback uint32) (uint32, error) {
} }
} }
} }
return fallback, nil return fallback, nil
} }
@@ -333,17 +334,115 @@ func parseScale(field, value string) (uint32, error) {
if clean == "" { if clean == "" {
return 0, merrors.InvalidArgument(field + " is empty") return 0, merrors.InvalidArgument(field + " is empty")
} }
parsed, err := strconv.ParseUint(clean, 10, 32) parsed, err := strconv.ParseUint(clean, 10, 32)
if err != nil { if err != nil {
return 0, merrors.InvalidArgument("invalid " + field + " value") return 0, merrors.InvalidArgument("invalid " + field + " value")
} }
return uint32(parsed), nil return uint32(parsed), nil
} }
func validateComputeInputs(plan *model.FeePlan, intent *feesv1.Intent) (*big.Rat, uint32, model.Trigger, error) {
if plan == nil {
return nil, 0, model.TriggerUnspecified, merrors.InvalidArgument("plan is required")
}
if intent == nil {
return nil, 0, model.TriggerUnspecified, merrors.InvalidArgument("intent is required")
}
trigger := convertTrigger(intent.GetTrigger())
if trigger == model.TriggerUnspecified {
return nil, 0, model.TriggerUnspecified, merrors.InvalidArgument("unsupported trigger")
}
baseAmount, err := dmath.RatFromString(intent.GetBaseAmount().GetAmount())
if err != nil {
return nil, 0, trigger, merrors.InvalidArgument("invalid base amount")
}
if baseAmount.Sign() < 0 {
return nil, 0, trigger, merrors.InvalidArgument("base amount cannot be negative")
}
return baseAmount, inferScale(intent.GetBaseAmount().GetAmount()), trigger, nil
}
func planIDFrom(plan *model.FeePlan) string {
if planRef := plan.GetID(); planRef != nil && !planRef.IsZero() {
return planRef.Hex()
}
return ""
}
func resolvedCurrency(baseCurrency, ruleCurrency string) string {
if override := strings.TrimSpace(ruleCurrency); override != "" {
return override
}
return baseCurrency
}
func resolvedEntrySide(rule model.FeeRule) accountingv1.EntrySide {
// Default fees to debit (i.e. charge the customer) when entry side is not specified.
if side := mapEntrySide(rule.EntrySide); side != accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED {
return side
}
return accountingv1.EntrySide_ENTRY_SIDE_DEBIT
}
func buildPostingLine(rule model.FeeRule, amount *big.Rat, scale uint32, currency string, entrySide accountingv1.EntrySide, planID string) *feesv1.DerivedPostingLine {
return &feesv1.DerivedPostingLine{
LedgerAccountRef: strings.TrimSpace(rule.LedgerAccountRef),
Money: &moneyv1.Money{
Amount: dmath.FormatRat(amount, scale),
Currency: currency,
},
LineType: mapLineType(rule.LineType),
Side: entrySide,
Meta: buildRuleMeta(rule, planID),
}
}
func buildAppliedRule(rule model.FeeRule, planID string) *feesv1.AppliedRule {
return &feesv1.AppliedRule{
RuleId: rule.RuleID,
RuleVersion: planID,
Formula: rule.Formula,
Rounding: mapRoundingMode(rule.Rounding),
TaxCode: metadataValue(rule.Metadata, "tax_code"),
TaxRate: metadataValue(rule.Metadata, "tax_rate"),
Parameters: cloneStringMap(rule.Metadata),
}
}
func buildRuleMeta(rule model.FeeRule, planID string) map[string]string {
meta := map[string]string{"fee_rule_id": rule.RuleID}
if planID != "" {
meta["fee_plan_id"] = planID
}
if rule.Metadata != nil {
if taxCode := strings.TrimSpace(rule.Metadata["tax_code"]); taxCode != "" {
meta["tax_code"] = taxCode
}
if taxRate := strings.TrimSpace(rule.Metadata["tax_rate"]); taxRate != "" {
meta["tax_rate"] = taxRate
}
}
return meta
}
func metadataValue(meta map[string]string, key string) string { func metadataValue(meta map[string]string, key string) string {
if meta == nil { if meta == nil {
return "" return ""
} }
return strings.TrimSpace(meta[key]) return strings.TrimSpace(meta[key])
} }
@@ -351,10 +450,10 @@ func cloneStringMap(src map[string]string) map[string]string {
if len(src) == 0 { if len(src) == 0 {
return nil return nil
} }
cloned := make(map[string]string, len(src)) cloned := make(map[string]string, len(src))
for k, v := range src { maps.Copy(cloned, src)
cloned[k] = v
}
return cloned return cloned
} }
@@ -362,18 +461,22 @@ func ruleMatchesAttributes(rule model.FeeRule, attributes map[string]string) boo
if len(rule.AppliesTo) == 0 { if len(rule.AppliesTo) == 0 {
return true return true
} }
for key, value := range rule.AppliesTo { for key, value := range rule.AppliesTo {
if attributes == nil { if attributes == nil {
return false return false
} }
attrValue, ok := attributes[key] attrValue, ok := attributes[key]
if !ok { if !ok {
return false return false
} }
if !matchesAttributeValue(value, attrValue) { if !matchesAttributeValue(value, attrValue) {
return false return false
} }
} }
return true return true
} }
@@ -383,16 +486,17 @@ func matchesAttributeValue(expected, actual string) bool {
return actual == "" return actual == ""
} }
values := strings.Split(trimmed, ",") for value := range strings.SplitSeq(trimmed, ",") {
for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue
} }
if value == "*" || value == actual { if value == "*" || value == actual {
return true return true
} }
} }
return false return false
} }
@@ -446,6 +550,8 @@ func mapRoundingMode(mode string) moneyv1.RoundingMode {
func convertTrigger(trigger feesv1.Trigger) model.Trigger { func convertTrigger(trigger feesv1.Trigger) model.Trigger {
switch trigger { switch trigger {
case feesv1.Trigger_TRIGGER_UNSPECIFIED:
return model.TriggerUnspecified
case feesv1.Trigger_TRIGGER_CAPTURE: case feesv1.Trigger_TRIGGER_CAPTURE:
return model.TriggerCapture return model.TriggerCapture
case feesv1.Trigger_TRIGGER_REFUND: case feesv1.Trigger_TRIGGER_REFUND:

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/billing/fees/storage" "github.com/tech/sendico/billing/fees/storage"
"github.com/tech/sendico/billing/fees/storage/model" "github.com/tech/sendico/billing/fees/storage/model"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
"github.com/tech/sendico/pkg/mlogger"
"github.com/tech/sendico/pkg/mutil/mzap" "github.com/tech/sendico/pkg/mutil/mzap"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
@@ -22,17 +23,19 @@ type planFinder interface {
type feeResolver struct { type feeResolver struct {
plans storage.PlansStore plans storage.PlansStore
finder planFinder finder planFinder
logger *zap.Logger logger mlogger.Logger
} }
func New(plans storage.PlansStore, logger *zap.Logger) *feeResolver { func New(plans storage.PlansStore, logger mlogger.Logger) *feeResolver {
var finder planFinder var finder planFinder
if pf, ok := plans.(planFinder); ok { if pf, ok := plans.(planFinder); ok {
finder = pf finder = pf
} }
if logger == nil { if logger == nil {
logger = zap.NewNop() logger = zap.NewNop()
} }
return &feeResolver{ return &feeResolver{
plans: plans, plans: plans,
finder: finder, finder: finder,
@@ -40,63 +43,100 @@ func New(plans storage.PlansStore, logger *zap.Logger) *feeResolver {
} }
} }
func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *bson.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) { func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *bson.ObjectID, trigger model.Trigger, asOf time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
if r.plans == nil { if r.plans == nil {
return nil, nil, merrors.InvalidArgument("fees: plans store is required") return nil, nil, merrors.InvalidArgument("fees: plans store is required")
} }
// Try org-specific first if provided. // Try org-specific first if provided.
if orgRef != nil && !orgRef.IsZero() { if isOrgRef(orgRef) {
if plan, err := r.getOrgPlan(ctx, *orgRef, at); err == nil { plan, rule, err := r.tryOrgRule(ctx, *orgRef, trigger, asOf, attrs)
if rule, selErr := selectRule(plan, trigger, at, attrs); selErr == nil { if err != nil {
return plan, rule, nil
} else if !errors.Is(selErr, ErrNoFeeRuleFound) {
r.logger.Warn("Failed selecting rule for org plan", zap.Error(selErr), mzap.ObjRef("org_ref", *orgRef))
return nil, nil, selErr
}
orgFields := []zap.Field{
mzap.ObjRef("org_ref", *orgRef),
zap.String("trigger", string(trigger)),
zap.Time("booked_at", at),
zap.Any("attributes", attrs),
}
orgFields = append(orgFields, zapFieldsForPlan(plan)...)
r.logger.Debug("No matching rule in org plan; falling back to global", orgFields...)
} else if !errors.Is(err, storage.ErrFeePlanNotFound) {
r.logger.Warn("Failed resolving org fee plan", zap.Error(err), mzap.ObjRef("org_ref", *orgRef))
return nil, nil, err return nil, nil, err
} }
if rule != nil {
return plan, rule, nil
}
} }
plan, err := r.getGlobalPlan(ctx, at) plan, err := r.getGlobalPlan(ctx, asOf)
if err != nil { if err != nil {
if errors.Is(err, storage.ErrFeePlanNotFound) { if errors.Is(err, storage.ErrFeePlanNotFound) {
r.logger.Debug("No applicable global fee plan found", zap.String("trigger", string(trigger)), r.logger.Debug("No applicable global fee plan found", zap.String("trigger", string(trigger)),
zap.Time("booked_at", at), zap.Any("attributes", attrs), zap.Time("booked_at", asOf), zap.Any("attributes", attrs),
) )
return nil, nil, merrors.NoData("fees: no applicable fee rule found") return nil, nil, merrors.NoData("fees: no applicable fee rule found")
} }
r.logger.Warn("Failed resolving global fee plan", zap.Error(err)) r.logger.Warn("Failed resolving global fee plan", zap.Error(err))
return nil, nil, err return nil, nil, err
} }
rule, err := selectRule(plan, trigger, at, attrs) rule, err := selectRule(plan, trigger, asOf, attrs)
if err != nil { if err != nil {
if !errors.Is(err, ErrNoFeeRuleFound) { if !errors.Is(err, ErrNoFeeRuleFound) {
r.logger.Warn("Failed selecting rule in global plan", zap.Error(err)) r.logger.Warn("Failed selecting rule in global plan", zap.Error(err))
} else { } else {
globalFields := []zap.Field{ globalFields := zapFieldsForPlan(plan)
globalFields = append([]zap.Field{
zap.String("trigger", string(trigger)), zap.String("trigger", string(trigger)),
zap.Time("booked_at", at), zap.Time("booked_at", asOf),
zap.Any("attributes", attrs), zap.Any("attributes", attrs),
} }, globalFields...)
globalFields = append(globalFields, zapFieldsForPlan(plan)...)
r.logger.Debug("No matching rule in global plan", globalFields...) r.logger.Debug("No matching rule in global plan", globalFields...)
} }
return nil, nil, err return nil, nil, err
} }
selectedFields := []zap.Field{ r.logSelectedRule(orgRef, trigger, asOf, attrs, rule, plan)
return plan, rule, nil
}
// tryOrgRule attempts to find a matching rule in the org-specific plan.
// Returns (plan, rule, nil) on success, (nil, nil, nil) to signal fallthrough to global,
// or (nil, nil, err) on a hard error.
func (r *feeResolver) tryOrgRule(ctx context.Context, orgRef bson.ObjectID, trigger model.Trigger, asOf time.Time, attrs map[string]string) (*model.FeePlan, *model.FeeRule, error) {
plan, err := r.getOrgPlan(ctx, orgRef, asOf)
if err != nil {
if errors.Is(err, storage.ErrFeePlanNotFound) {
return nil, nil, nil
}
r.logger.Warn("Failed resolving org fee plan", zap.Error(err), mzap.ObjRef("org_ref", orgRef))
return nil, nil, err
}
rule, selErr := selectRule(plan, trigger, asOf, attrs)
if selErr == nil {
return plan, rule, nil
}
if !errors.Is(selErr, ErrNoFeeRuleFound) {
r.logger.Warn("Failed selecting rule for org plan", zap.Error(selErr), mzap.ObjRef("org_ref", orgRef))
return nil, nil, selErr
}
orgFields := zapFieldsForPlan(plan)
orgFields = append([]zap.Field{
mzap.ObjRef("org_ref", orgRef),
zap.String("trigger", string(trigger)),
zap.Time("booked_at", asOf),
zap.Any("attributes", attrs),
}, orgFields...)
r.logger.Debug("No matching rule in org plan; falling back to global", orgFields...)
return nil, nil, nil
}
func (r *feeResolver) logSelectedRule(orgRef *bson.ObjectID, trigger model.Trigger, at time.Time, attrs map[string]string, rule *model.FeeRule, plan *model.FeePlan) {
fields := []zap.Field{
zap.String("trigger", string(trigger)), zap.String("trigger", string(trigger)),
zap.Time("booked_at", at), zap.Time("booked_at", at),
zap.Any("attributes", attrs), zap.Any("attributes", attrs),
@@ -106,59 +146,65 @@ func (r *feeResolver) ResolveFeeRule(ctx context.Context, orgRef *bson.ObjectID,
zap.Time("rule_effective_from", rule.EffectiveFrom), zap.Time("rule_effective_from", rule.EffectiveFrom),
} }
if rule.EffectiveTo != nil { if rule.EffectiveTo != nil {
selectedFields = append(selectedFields, zap.Time("rule_effective_to", *rule.EffectiveTo)) fields = append(fields, zap.Time("rule_effective_to", *rule.EffectiveTo))
} }
if orgRef != nil && !orgRef.IsZero() {
selectedFields = append(selectedFields, mzap.ObjRef("org_ref", *orgRef))
}
selectedFields = append(selectedFields, zapFieldsForPlan(plan)...)
r.logger.Debug("Selected fee rule", selectedFields...)
return plan, rule, nil if isOrgRef(orgRef) {
fields = append(fields, mzap.ObjRef("org_ref", *orgRef))
}
fields = append(fields, zapFieldsForPlan(plan)...)
r.logger.Debug("Selected fee rule", fields...)
}
func isOrgRef(ref *bson.ObjectID) bool {
return ref != nil && !ref.IsZero()
} }
func (r *feeResolver) getOrgPlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (r *feeResolver) getOrgPlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) {
if r.finder != nil { if r.finder != nil {
return r.finder.FindActiveOrgPlan(ctx, orgRef, at) return r.finder.FindActiveOrgPlan(ctx, orgRef, at)
} }
return r.plans.GetActivePlan(ctx, orgRef, at) return r.plans.GetActivePlan(ctx, orgRef, at)
} }
func (r *feeResolver) getGlobalPlan(ctx context.Context, at time.Time) (*model.FeePlan, error) { func (r *feeResolver) getGlobalPlan(ctx context.Context, asOf time.Time) (*model.FeePlan, error) {
if r.finder != nil { if r.finder != nil {
return r.finder.FindActiveGlobalPlan(ctx, at) return r.finder.FindActiveGlobalPlan(ctx, asOf)
} }
// Treat zero ObjectID as global in legacy path. // Treat zero ObjectID as global in legacy path.
return r.plans.GetActivePlan(ctx, bson.NilObjectID, at) return r.plans.GetActivePlan(ctx, bson.NilObjectID, asOf)
} }
func selectRule(plan *model.FeePlan, trigger model.Trigger, at time.Time, attrs map[string]string) (*model.FeeRule, error) { func selectRule(plan *model.FeePlan, trigger model.Trigger, asOf time.Time, attrs map[string]string) (*model.FeeRule, error) {
if plan == nil { if plan == nil {
return nil, merrors.NoData("fees: no applicable fee rule found") return nil, merrors.NoData("fees: no applicable fee rule found")
} }
var selected *model.FeeRule var (
var highestPriority int selected *model.FeeRule
highestPriority int
)
for _, rule := range plan.Rules { for _, rule := range plan.Rules {
if rule.Trigger != trigger { if !ruleIsActive(rule, trigger, asOf) {
continue
}
if rule.EffectiveFrom.After(at) {
continue
}
if rule.EffectiveTo != nil && !rule.EffectiveTo.After(at) {
continue continue
} }
if !matchesAppliesTo(rule.AppliesTo, attrs) { if !matchesAppliesTo(rule.AppliesTo, attrs) {
continue continue
} }
if selected == nil || rule.Priority > highestPriority { if selected == nil || rule.Priority > highestPriority {
copy := rule matched := rule
selected = &copy selected = &matched
highestPriority = rule.Priority highestPriority = rule.Priority
continue continue
} }
if rule.Priority == highestPriority { if rule.Priority == highestPriority {
return nil, merrors.DataConflict("fees: conflicting fee rules") return nil, merrors.DataConflict("fees: conflicting fee rules")
} }
@@ -167,25 +213,46 @@ func selectRule(plan *model.FeePlan, trigger model.Trigger, at time.Time, attrs
if selected == nil { if selected == nil {
return nil, merrors.NoData("fees: no applicable fee rule found") return nil, merrors.NoData("fees: no applicable fee rule found")
} }
return selected, nil return selected, nil
} }
func ruleIsActive(rule model.FeeRule, trigger model.Trigger, asOf time.Time) bool {
if rule.Trigger != trigger {
return false
}
if rule.EffectiveFrom.After(asOf) {
return false
}
if rule.EffectiveTo != nil && !rule.EffectiveTo.After(asOf) {
return false
}
return true
}
func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool { func matchesAppliesTo(appliesTo map[string]string, attrs map[string]string) bool {
if len(appliesTo) == 0 { if len(appliesTo) == 0 {
return true return true
} }
for key, value := range appliesTo { for key, value := range appliesTo {
if attrs == nil { if attrs == nil {
return false return false
} }
attrValue, ok := attrs[key] attrValue, ok := attrs[key]
if !ok { if !ok {
return false return false
} }
if !matchesAppliesValue(value, attrValue) { if !matchesAppliesValue(value, attrValue) {
return false return false
} }
} }
return true return true
} }
@@ -195,16 +262,17 @@ func matchesAppliesValue(expected, actual string) bool {
return actual == "" return actual == ""
} }
values := strings.Split(trimmed, ",") for value := range strings.SplitSeq(trimmed, ",") {
for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue
} }
if value == "*" || value == actual { if value == "*" || value == actual {
return true return true
} }
} }
return false return false
} }
@@ -212,6 +280,7 @@ func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
if plan == nil { if plan == nil {
return []zap.Field{zap.Bool("plan_present", false)} return []zap.Field{zap.Bool("plan_present", false)}
} }
fields := []zap.Field{ fields := []zap.Field{
zap.Bool("plan_present", true), zap.Bool("plan_present", true),
zap.Bool("plan_active", plan.Active), zap.Bool("plan_active", plan.Active),
@@ -223,13 +292,16 @@ func zapFieldsForPlan(plan *model.FeePlan) []zap.Field {
} else { } else {
fields = append(fields, zap.Bool("plan_effective_to_set", false)) fields = append(fields, zap.Bool("plan_effective_to_set", false))
} }
if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() { if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
fields = append(fields, mzap.ObjRef("plan_org_ref", *plan.OrganizationRef)) fields = append(fields, mzap.ObjRef("plan_org_ref", *plan.OrganizationRef))
} else { } else {
fields = append(fields, zap.Bool("plan_org_ref_set", false)) fields = append(fields, zap.Bool("plan_org_ref_set", false))
} }
if plan.GetID() != nil && !plan.GetID().IsZero() { if plan.GetID() != nil && !plan.GetID().IsZero() {
fields = append(fields, mzap.StorableRef(plan)) fields = append(fields, mzap.StorableRef(plan))
} }
return fields return fields
} }

View File

@@ -13,7 +13,7 @@ import (
) )
func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) { func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
globalPlan := &model.FeePlan{ globalPlan := &model.FeePlan{
@@ -28,20 +28,23 @@ func TestResolver_GlobalFallbackWhenOrgMissing(t *testing.T) {
resolver := New(store, zap.NewNop()) resolver := New(store, zap.NewNop())
orgA := bson.NewObjectID() orgA := bson.NewObjectID()
plan, rule, err := resolver.ResolveFeeRule(context.Background(), &orgA, model.TriggerCapture, now, nil) plan, rule, err := resolver.ResolveFeeRule(context.Background(), &orgA, model.TriggerCapture, now, nil)
if err != nil { if err != nil {
t.Fatalf("expected fallback to global, got error: %v", err) t.Fatalf("expected fallback to global, got error: %v", err)
} }
if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() { if plan.OrganizationRef != nil && !plan.OrganizationRef.IsZero() {
t.Fatalf("expected global plan, got orgRef %s", plan.OrganizationRef.Hex()) t.Fatalf("expected global plan, got orgRef %s", plan.OrganizationRef.Hex())
} }
if rule.RuleID != "global_capture" { if rule.RuleID != "global_capture" {
t.Fatalf("unexpected rule selected: %s", rule.RuleID) t.Fatalf("unexpected rule selected: %s", rule.RuleID)
} }
} }
func TestResolver_OrgOverridesGlobal(t *testing.T) { func TestResolver_OrgOverridesGlobal(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
@@ -67,22 +70,25 @@ func TestResolver_OrgOverridesGlobal(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected org plan rule, got error: %v", err) t.Fatalf("expected org plan rule, got error: %v", err)
} }
if rule.RuleID != "org_capture" { if rule.RuleID != "org_capture" {
t.Fatalf("expected org rule, got %s", rule.RuleID) t.Fatalf("expected org rule, got %s", rule.RuleID)
} }
otherOrg := bson.NewObjectID() otherOrg := bson.NewObjectID()
_, rule, err = resolver.ResolveFeeRule(context.Background(), &otherOrg, model.TriggerCapture, now, nil) _, rule, err = resolver.ResolveFeeRule(context.Background(), &otherOrg, model.TriggerCapture, now, nil)
if err != nil { if err != nil {
t.Fatalf("expected global fallback for other org, got error: %v", err) t.Fatalf("expected global fallback for other org, got error: %v", err)
} }
if rule.RuleID != "global_capture" { if rule.RuleID != "global_capture" {
t.Fatalf("expected global rule, got %s", rule.RuleID) t.Fatalf("expected global rule, got %s", rule.RuleID)
} }
} }
func TestResolver_SelectsHighestPriority(t *testing.T) { func TestResolver_SelectsHighestPriority(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
@@ -103,6 +109,7 @@ func TestResolver_SelectsHighestPriority(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected rule resolution, got error: %v", err) t.Fatalf("expected rule resolution, got error: %v", err)
} }
if rule.RuleID != "high" { if rule.RuleID != "high" {
t.Fatalf("expected highest priority rule, got %s", rule.RuleID) t.Fatalf("expected highest priority rule, got %s", rule.RuleID)
} }
@@ -121,7 +128,7 @@ func TestResolver_SelectsHighestPriority(t *testing.T) {
} }
func TestResolver_EffectiveDateFiltering(t *testing.T) { func TestResolver_EffectiveDateFiltering(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
@@ -152,13 +159,14 @@ func TestResolver_EffectiveDateFiltering(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected fallback to global, got error: %v", err) t.Fatalf("expected fallback to global, got error: %v", err)
} }
if rule.RuleID != "current" { if rule.RuleID != "current" {
t.Fatalf("expected current global rule, got %s", rule.RuleID) t.Fatalf("expected current global rule, got %s", rule.RuleID)
} }
} }
func TestResolver_AppliesToFiltering(t *testing.T) { func TestResolver_AppliesToFiltering(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
plan := &model.FeePlan{ plan := &model.FeePlan{
@@ -176,6 +184,7 @@ func TestResolver_AppliesToFiltering(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected card rule, got error: %v", err) t.Fatalf("expected card rule, got error: %v", err)
} }
if rule.RuleID != "card" { if rule.RuleID != "card" {
t.Fatalf("expected card rule, got %s", rule.RuleID) t.Fatalf("expected card rule, got %s", rule.RuleID)
} }
@@ -184,13 +193,14 @@ func TestResolver_AppliesToFiltering(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected default rule, got error: %v", err) t.Fatalf("expected default rule, got error: %v", err)
} }
if rule.RuleID != "default" { if rule.RuleID != "default" {
t.Fatalf("expected default rule, got %s", rule.RuleID) t.Fatalf("expected default rule, got %s", rule.RuleID)
} }
} }
func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) { func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
plan := &model.FeePlan{ plan := &model.FeePlan{
@@ -209,6 +219,7 @@ func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected list match rule, got error: %v", err) t.Fatalf("expected list match rule, got error: %v", err)
} }
if rule.RuleID != "network_multi" { if rule.RuleID != "network_multi" {
t.Fatalf("expected network list rule, got %s", rule.RuleID) t.Fatalf("expected network list rule, got %s", rule.RuleID)
} }
@@ -217,6 +228,7 @@ func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected wildcard rule, got error: %v", err) t.Fatalf("expected wildcard rule, got error: %v", err)
} }
if rule.RuleID != "asset_any" { if rule.RuleID != "asset_any" {
t.Fatalf("expected asset wildcard rule, got %s", rule.RuleID) t.Fatalf("expected asset wildcard rule, got %s", rule.RuleID)
} }
@@ -225,13 +237,14 @@ func TestResolver_AppliesToFilteringSupportsListsAndWildcard(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("expected default rule, got error: %v", err) t.Fatalf("expected default rule, got error: %v", err)
} }
if rule.RuleID != "default" { if rule.RuleID != "default" {
t.Fatalf("expected default rule, got %s", rule.RuleID) t.Fatalf("expected default rule, got %s", rule.RuleID)
} }
} }
func TestResolver_MissingTriggerReturnsErr(t *testing.T) { func TestResolver_MissingTriggerReturnsErr(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
plan := &model.FeePlan{ plan := &model.FeePlan{
@@ -250,28 +263,28 @@ func TestResolver_MissingTriggerReturnsErr(t *testing.T) {
} }
func TestResolver_MultipleActivePlansConflict(t *testing.T) { func TestResolver_MultipleActivePlansConflict(t *testing.T) {
t.Helper() t.Parallel()
now := time.Now() now := time.Now()
org := bson.NewObjectID() org := bson.NewObjectID()
p1 := &model.FeePlan{ plan1 := &model.FeePlan{
Active: true, Active: true,
EffectiveFrom: now.Add(-time.Hour), EffectiveFrom: now.Add(-time.Hour),
Rules: []model.FeeRule{ Rules: []model.FeeRule{
{RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)}, {RuleID: "r1", Trigger: model.TriggerCapture, Priority: 10, Percentage: "0.05", EffectiveFrom: now.Add(-time.Hour)},
}, },
} }
p1.OrganizationRef = &org plan1.OrganizationRef = &org
p2 := &model.FeePlan{ plan2 := &model.FeePlan{
Active: true, Active: true,
EffectiveFrom: now.Add(-30 * time.Minute), EffectiveFrom: now.Add(-30 * time.Minute),
Rules: []model.FeeRule{ Rules: []model.FeeRule{
{RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)}, {RuleID: "r2", Trigger: model.TriggerCapture, Priority: 20, Percentage: "0.03", EffectiveFrom: now.Add(-time.Hour)},
}, },
} }
p2.OrganizationRef = &org plan2.OrganizationRef = &org
store := &memoryPlansStore{plans: []*model.FeePlan{p1, p2}} store := &memoryPlansStore{plans: []*model.FeePlan{plan1, plan2}}
resolver := New(store, zap.NewNop()) resolver := New(store, zap.NewNop())
if _, _, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil); !errors.Is(err, storage.ErrConflictingFeePlans) { if _, _, err := resolver.ResolveFeeRule(context.Background(), &org, model.TriggerCapture, now, nil); !errors.Is(err, storage.ErrConflictingFeePlans) {
@@ -289,66 +302,83 @@ func (m *memoryPlansStore) Get(context.Context, bson.ObjectID) (*model.FeePlan,
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
func (m *memoryPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (m *memoryPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
if !orgRef.IsZero() { if !orgRef.IsZero() {
if plan, err := m.FindActiveOrgPlan(ctx, orgRef, at); err == nil { if plan, err := m.FindActiveOrgPlan(ctx, orgRef, asOf); err == nil {
return plan, nil return plan, nil
} else if !errors.Is(err, storage.ErrFeePlanNotFound) { } else if !errors.Is(err, storage.ErrFeePlanNotFound) {
return nil, err return nil, err
} }
} }
return m.FindActiveGlobalPlan(ctx, at)
return m.FindActiveGlobalPlan(ctx, asOf)
} }
func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (m *memoryPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
var matches []*model.FeePlan var matches []*model.FeePlan
for _, plan := range m.plans { for _, plan := range m.plans {
if plan == nil || plan.OrganizationRef == nil || plan.OrganizationRef.IsZero() || (*plan.OrganizationRef != orgRef) { if plan == nil || plan.OrganizationRef == nil || plan.OrganizationRef.IsZero() || (*plan.OrganizationRef != orgRef) {
continue continue
} }
if !plan.Active { if !plan.Active {
continue continue
} }
if plan.EffectiveFrom.After(at) {
if plan.EffectiveFrom.After(asOf) {
continue continue
} }
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(at) {
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(asOf) {
continue continue
} }
matches = append(matches, plan) matches = append(matches, plan)
} }
if len(matches) == 0 { if len(matches) == 0 {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if len(matches) > 1 { if len(matches) > 1 {
return nil, storage.ErrConflictingFeePlans return nil, storage.ErrConflictingFeePlans
} }
return matches[0], nil return matches[0], nil
} }
func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, at time.Time) (*model.FeePlan, error) { func (m *memoryPlansStore) FindActiveGlobalPlan(_ context.Context, asOf time.Time) (*model.FeePlan, error) {
var matches []*model.FeePlan var matches []*model.FeePlan
for _, plan := range m.plans { for _, plan := range m.plans {
if plan == nil || ((plan.OrganizationRef != nil) && !plan.OrganizationRef.IsZero()) { if plan == nil || ((plan.OrganizationRef != nil) && !plan.OrganizationRef.IsZero()) {
continue continue
} }
if !plan.Active { if !plan.Active {
continue continue
} }
if plan.EffectiveFrom.After(at) {
if plan.EffectiveFrom.After(asOf) {
continue continue
} }
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(at) {
if plan.EffectiveTo != nil && !plan.EffectiveTo.After(asOf) {
continue continue
} }
matches = append(matches, plan) matches = append(matches, plan)
} }
if len(matches) == 0 { if len(matches) == 0 {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if len(matches) > 1 { if len(matches) > 1 {
return nil, storage.ErrConflictingFeePlans return nil, storage.ErrConflictingFeePlans
} }
return matches[0], nil return matches[0], nil
} }

View File

@@ -12,6 +12,7 @@ import (
func requestLogFields(meta *feesv1.RequestMeta, intent *feesv1.Intent) []zap.Field { func requestLogFields(meta *feesv1.RequestMeta, intent *feesv1.Intent) []zap.Field {
fields := logFieldsFromRequestMeta(meta) fields := logFieldsFromRequestMeta(meta)
fields = append(fields, logFieldsFromIntent(intent)...) fields = append(fields, logFieldsFromIntent(intent)...)
return fields return fields
} }
@@ -19,11 +20,14 @@ func logFieldsFromRequestMeta(meta *feesv1.RequestMeta) []zap.Field {
if meta == nil { if meta == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 4) fields := make([]zap.Field, 0, 4)
if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" { if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" {
fields = append(fields, zap.String("organization_ref", org)) fields = append(fields, zap.String("organization_ref", org))
} }
fields = append(fields, logFieldsFromTrace(meta.GetTrace())...) fields = append(fields, logFieldsFromTrace(meta.GetTrace())...)
return fields return fields
} }
@@ -31,24 +35,30 @@ func logFieldsFromIntent(intent *feesv1.Intent) []zap.Field {
if intent == nil { if intent == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 5) fields := make([]zap.Field, 0, 5)
if trigger := intent.GetTrigger(); trigger != feesv1.Trigger_TRIGGER_UNSPECIFIED { if trigger := intent.GetTrigger(); trigger != feesv1.Trigger_TRIGGER_UNSPECIFIED {
fields = append(fields, zap.String("trigger", trigger.String())) fields = append(fields, zap.String("trigger", trigger.String()))
} }
if base := intent.GetBaseAmount(); base != nil { if base := intent.GetBaseAmount(); base != nil {
if amount := strings.TrimSpace(base.GetAmount()); amount != "" { if amount := strings.TrimSpace(base.GetAmount()); amount != "" {
fields = append(fields, zap.String("base_amount", amount)) fields = append(fields, zap.String("base_amount", amount))
} }
if currency := strings.TrimSpace(base.GetCurrency()); currency != "" { if currency := strings.TrimSpace(base.GetCurrency()); currency != "" {
fields = append(fields, zap.String("base_currency", currency)) fields = append(fields, zap.String("base_currency", currency))
} }
} }
if booked := intent.GetBookedAt(); booked != nil && booked.IsValid() { if booked := intent.GetBookedAt(); booked != nil && booked.IsValid() {
fields = append(fields, zap.Time("booked_at", booked.AsTime())) fields = append(fields, zap.Time("booked_at", booked.AsTime()))
} }
if attrs := intent.GetAttributes(); len(attrs) > 0 { if attrs := intent.GetAttributes(); len(attrs) > 0 {
fields = append(fields, zap.Int("attributes_count", len(attrs))) fields = append(fields, zap.Int("attributes_count", len(attrs)))
} }
return fields return fields
} }
@@ -56,16 +66,20 @@ func logFieldsFromTrace(trace *tracev1.TraceContext) []zap.Field {
if trace == nil { if trace == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 3) fields := make([]zap.Field, 0, 3)
if reqRef := strings.TrimSpace(trace.GetRequestRef()); reqRef != "" { if reqRef := strings.TrimSpace(trace.GetRequestRef()); reqRef != "" {
fields = append(fields, zap.String("request_ref", reqRef)) fields = append(fields, zap.String("request_ref", reqRef))
} }
if idem := strings.TrimSpace(trace.GetIdempotencyKey()); idem != "" { if idem := strings.TrimSpace(trace.GetIdempotencyKey()); idem != "" {
fields = append(fields, zap.String("idempotency_key", idem)) fields = append(fields, zap.String("idempotency_key", idem))
} }
if traceRef := strings.TrimSpace(trace.GetTraceRef()); traceRef != "" { if traceRef := strings.TrimSpace(trace.GetTraceRef()); traceRef != "" {
fields = append(fields, zap.String("trace_ref", traceRef)) fields = append(fields, zap.String("trace_ref", traceRef))
} }
return fields return fields
} }
@@ -73,16 +87,20 @@ func logFieldsFromTokenPayload(payload *feeQuoteTokenPayload) []zap.Field {
if payload == nil { if payload == nil {
return nil return nil
} }
fields := make([]zap.Field, 0, 6) fields := make([]zap.Field, 0, 6)
if org := strings.TrimSpace(payload.OrganizationRef); org != "" { if org := strings.TrimSpace(payload.OrganizationRef); org != "" {
fields = append(fields, zap.String("organization_ref", org)) fields = append(fields, zap.String("organization_ref", org))
} }
if payload.ExpiresAtUnixMs > 0 { if payload.ExpiresAtUnixMs > 0 {
fields = append(fields, fields = append(fields,
zap.Int64("expires_at_unix_ms", payload.ExpiresAtUnixMs), zap.Int64("expires_at_unix_ms", payload.ExpiresAtUnixMs),
zap.Time("expires_at", time.UnixMilli(payload.ExpiresAtUnixMs))) zap.Time("expires_at", time.UnixMilli(payload.ExpiresAtUnixMs)))
} }
fields = append(fields, logFieldsFromIntent(payload.Intent)...) fields = append(fields, logFieldsFromIntent(payload.Intent)...)
fields = append(fields, logFieldsFromTrace(payload.Trace)...) fields = append(fields, logFieldsFromTrace(payload.Trace)...)
return fields return fields
} }

View File

@@ -50,6 +50,7 @@ func observeMetrics(call string, trigger feesv1.Trigger, statusLabel string, fxU
if trigger == feesv1.Trigger_TRIGGER_UNSPECIFIED { if trigger == feesv1.Trigger_TRIGGER_UNSPECIFIED {
triggerLabel = "TRIGGER_UNSPECIFIED" triggerLabel = "TRIGGER_UNSPECIFIED"
} }
fxLabel := strconv.FormatBool(fxUsed) fxLabel := strconv.FormatBool(fxUsed)
quoteRequestsTotal.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Inc() quoteRequestsTotal.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Inc()
quoteLatency.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Observe(took.Seconds()) quoteLatency.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Observe(took.Seconds())
@@ -59,13 +60,16 @@ func statusFromError(err error) string {
if err == nil { if err == nil {
return "success" return "success"
} }
st, ok := status.FromError(err) st, ok := status.FromError(err)
if !ok { if !ok {
return "error" return "error"
} }
code := st.Code() code := st.Code()
if code == codes.OK { if code == codes.OK {
return "success" return "success"
} }
return strings.ToLower(code.String()) return strings.ToLower(code.String())
} }

View File

@@ -5,7 +5,6 @@ import (
"encoding/base64" "encoding/base64"
"encoding/json" "encoding/json"
"errors" "errors"
"fmt"
"strings" "strings"
"time" "time"
@@ -22,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"
@@ -33,6 +33,8 @@ import (
) )
type Service struct { type Service struct {
feesv1.UnimplementedFeeEngineServer
logger mlogger.Logger logger mlogger.Logger
storage storage.Repository storage storage.Repository
producer msg.Producer producer msg.Producer
@@ -42,7 +44,6 @@ type Service struct {
resolver FeeResolver resolver FeeResolver
announcer *discovery.Announcer announcer *discovery.Announcer
invokeURI string invokeURI string
feesv1.UnimplementedFeeEngineServer
} }
func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service { func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service {
@@ -52,6 +53,7 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
producer: producer, producer: producer,
clock: clockpkg.NewSystem(), clock: clockpkg.NewSystem(),
} }
initMetrics() initMetrics()
for _, opt := range opts { for _, opt := range opts {
@@ -61,9 +63,11 @@ func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Pro
if svc.clock == nil { if svc.clock == nil {
svc.clock = clockpkg.NewSystem() svc.clock = clockpkg.NewSystem()
} }
if svc.calculator == nil { if svc.calculator == nil {
svc.calculator = internalcalculator.New(svc.logger, svc.oracle) svc.calculator = internalcalculator.New(svc.logger, svc.oracle)
} }
if svc.resolver == nil { if svc.resolver == nil {
svc.resolver = resolver.New(repo.Plans(), svc.logger) svc.resolver = resolver.New(repo.Plans(), svc.logger)
} }
@@ -83,25 +87,12 @@ func (s *Service) Shutdown() {
if s == nil { if s == nil {
return return
} }
if s.announcer != nil { if s.announcer != nil {
s.announcer.Stop() s.announcer.Stop()
} }
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: "BILLING_FEES",
Operations: []string{"fee.calc"},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, string(mservice.FeePlans), announce)
s.announcer.Start()
}
func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) { func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) {
var ( var (
meta *feesv1.RequestMeta meta *feesv1.RequestMeta
@@ -111,23 +102,29 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
meta = req.GetMeta() meta = req.GetMeta()
intent = req.GetIntent() intent = req.GetIntent()
} }
logger := s.logger.With(requestLogFields(meta, intent)...) logger := s.logger.With(requestLogFields(meta, intent)...)
start := s.clock.Now() start := s.clock.Now()
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
if intent != nil { if intent != nil {
trigger = intent.GetTrigger() trigger = intent.GetTrigger()
} }
var fxUsed bool var fxUsed bool
defer func() { defer func() {
statusLabel := statusFromError(err) statusLabel := statusFromError(err)
linesCount := 0 linesCount := 0
appliedCount := 0 appliedCount := 0
if err == nil && resp != nil { if err == nil && resp != nil {
fxUsed = resp.GetFxUsed() != nil fxUsed = resp.GetFxUsed() != nil
linesCount = len(resp.GetLines()) linesCount = len(resp.GetLines())
appliedCount = len(resp.GetApplied()) appliedCount = len(resp.GetApplied())
} }
observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start)) observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{ logFields := []zap.Field{
@@ -140,8 +137,10 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
} }
if err != nil { if err != nil {
logger.Warn("QuoteFees finished", append(logFields, zap.Error(err))...) logger.Warn("QuoteFees finished", append(logFields, zap.Error(err))...)
return return
} }
logger.Info("QuoteFees finished", logFields...) logger.Info("QuoteFees finished", logFields...)
}() }()
@@ -155,12 +154,14 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
if parseErr != nil { if parseErr != nil {
logger.Warn("QuoteFees invalid organization_ref", zap.Error(parseErr)) logger.Warn("QuoteFees invalid organization_ref", zap.Error(parseErr))
err = status.Error(codes.InvalidArgument, "invalid organization_ref") err = status.Error(codes.InvalidArgument, "invalid organization_ref")
return nil, err return nil, err
} }
lines, applied, fx, computeErr := s.computeQuote(ctx, orgRef, req.GetIntent(), req.GetPolicy(), req.GetMeta().GetTrace()) lines, applied, fxResult, computeErr := s.computeQuote(ctx, orgRef, req.GetIntent(), req.GetPolicy(), req.GetMeta().GetTrace())
if computeErr != nil { if computeErr != nil {
err = computeErr err = computeErr
return nil, err return nil, err
} }
@@ -168,8 +169,9 @@ func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (
Meta: &feesv1.ResponseMeta{Trace: req.GetMeta().GetTrace()}, Meta: &feesv1.ResponseMeta{Trace: req.GetMeta().GetTrace()},
Lines: lines, Lines: lines,
Applied: applied, Applied: applied,
FxUsed: fx, FxUsed: fxResult,
} }
return resp, nil return resp, nil
} }
@@ -182,48 +184,17 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
meta = req.GetMeta() meta = req.GetMeta()
intent = req.GetIntent() intent = req.GetIntent()
} }
logger := s.logger.With(requestLogFields(meta, intent)...) logger := s.logger.With(requestLogFields(meta, intent)...)
start := s.clock.Now() start := s.clock.Now()
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
if intent != nil { if intent != nil {
trigger = intent.GetTrigger() trigger = intent.GetTrigger()
} }
var (
fxUsed bool
expiresAt time.Time
)
defer func() {
statusLabel := statusFromError(err)
linesCount := 0
appliedCount := 0
if err == nil && resp != nil {
fxUsed = resp.GetFxUsed() != nil
linesCount = len(resp.GetLines())
appliedCount = len(resp.GetApplied())
if ts := resp.GetExpiresAt(); ts != nil {
expiresAt = ts.AsTime()
}
}
observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{ defer func() { s.observePrecomputeFees(logger, err, resp, trigger, start) }()
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Int("lines", linesCount),
zap.Int("applied_rules", appliedCount),
}
if !expiresAt.IsZero() {
logFields = append(logFields, zap.Time("expires_at", expiresAt))
}
if err != nil {
logger.Warn("PrecomputeFees finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("PrecomputeFees finished", logFields...)
}()
logger.Debug("PrecomputeFees request received") logger.Debug("PrecomputeFees request received")
@@ -237,12 +208,14 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
if parseErr != nil { if parseErr != nil {
logger.Warn("PrecomputeFees invalid organization_ref", zap.Error(parseErr)) logger.Warn("PrecomputeFees invalid organization_ref", zap.Error(parseErr))
err = status.Error(codes.InvalidArgument, "invalid organization_ref") err = status.Error(codes.InvalidArgument, "invalid organization_ref")
return nil, err return nil, err
} }
lines, applied, fx, computeErr := s.computeQuoteWithTime(ctx, orgRef, req.GetIntent(), nil, req.GetMeta().GetTrace(), now) lines, applied, fxResult, computeErr := s.computeQuoteWithTime(ctx, orgRef, req.GetIntent(), nil, req.GetMeta().GetTrace(), now)
if computeErr != nil { if computeErr != nil {
err = computeErr err = computeErr
return nil, err return nil, err
} }
@@ -250,7 +223,8 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
if ttl <= 0 { if ttl <= 0 {
ttl = 60000 ttl = 60000
} }
expiresAt = now.Add(time.Duration(ttl) * time.Millisecond)
expiresAt := now.Add(time.Duration(ttl) * time.Millisecond)
payload := feeQuoteTokenPayload{ payload := feeQuoteTokenPayload{
OrganizationRef: req.GetMeta().GetOrganizationRef(), OrganizationRef: req.GetMeta().GetOrganizationRef(),
@@ -261,8 +235,9 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
var token string var token string
if token, err = encodeTokenPayload(payload); err != nil { if token, err = encodeTokenPayload(payload); err != nil {
logger.Warn("failed to encode fee quote token", zap.Error(err)) logger.Warn("Failed to encode fee quote token", zap.Error(err))
err = status.Error(codes.Internal, "failed to encode fee quote token") err = status.Error(codes.Internal, "failed to encode fee quote token")
return nil, err return nil, err
} }
@@ -272,8 +247,9 @@ func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFees
ExpiresAt: timestamppb.New(expiresAt), ExpiresAt: timestamppb.New(expiresAt),
Lines: lines, Lines: lines,
Applied: applied, Applied: applied,
FxUsed: fx, FxUsed: fxResult,
} }
return resp, nil return resp, nil
} }
@@ -282,49 +258,23 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
if req != nil { if req != nil {
tokenLen = len(strings.TrimSpace(req.GetFeeQuoteToken())) tokenLen = len(strings.TrimSpace(req.GetFeeQuoteToken()))
} }
logger := s.logger.With(zap.Int("token_length", tokenLen)) logger := s.logger.With(zap.Int("token_length", tokenLen))
start := s.clock.Now() start := s.clock.Now()
trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
var (
fxUsed bool
resultReason string
)
defer func() {
statusLabel := statusFromError(err)
if err == nil && resp != nil {
if !resp.GetValid() {
statusLabel = "invalid"
}
fxUsed = resp.GetFxUsed() != nil
if resp.GetIntent() != nil {
trigger = resp.GetIntent().GetTrigger()
}
}
observeMetrics("validate", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{ trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)), var resultReason string
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()), defer func() { s.observeValidateFeeToken(logger, err, resp, trigger, resultReason, start) }()
zap.Bool("valid", resp != nil && resp.GetValid()),
}
if resultReason != "" {
logFields = append(logFields, zap.String("reason", resultReason))
}
if err != nil {
logger.Warn("ValidateFeeToken finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("ValidateFeeToken finished", logFields...)
}()
logger.Debug("ValidateFeeToken request received") logger.Debug("ValidateFeeToken request received")
if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" { if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" {
resultReason = "missing_token" resultReason = "missing_token"
err = status.Error(codes.InvalidArgument, "fee_quote_token is required") err = status.Error(codes.InvalidArgument, "fee_quote_token is required")
return nil, err return nil, err
} }
@@ -333,8 +283,11 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken()) payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken())
if decodeErr != nil { if decodeErr != nil {
resultReason = "invalid_token" resultReason = "invalid_token"
logger.Warn("failed to decode fee quote token", zap.Error(decodeErr))
logger.Warn("Failed to decode fee quote token", zap.Error(decodeErr))
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
return resp, nil return resp, nil
} }
@@ -346,22 +299,29 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
if now.UnixMilli() > payload.ExpiresAtUnixMs { if now.UnixMilli() > payload.ExpiresAtUnixMs {
resultReason = "expired" resultReason = "expired"
logger.Info("fee quote token expired")
logger.Info("Fee quote token expired")
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"}
return resp, nil return resp, nil
} }
orgRef, parseErr := bson.ObjectIDFromHex(payload.OrganizationRef) orgRef, parseErr := bson.ObjectIDFromHex(payload.OrganizationRef)
if parseErr != nil { if parseErr != nil {
resultReason = "invalid_token" resultReason = "invalid_token"
logger.Warn("token contained invalid organization reference", zap.Error(parseErr))
logger.Warn("Token contained invalid organization reference", zap.Error(parseErr))
resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"}
return resp, nil return resp, nil
} }
lines, applied, fx, computeErr := s.computeQuoteWithTime(ctx, orgRef, payload.Intent, nil, payload.Trace, now) lines, applied, fxResult, computeErr := s.computeQuoteWithTime(ctx, orgRef, payload.Intent, nil, payload.Trace, now)
if computeErr != nil { if computeErr != nil {
err = computeErr err = computeErr
return nil, err return nil, err
} }
@@ -371,8 +331,9 @@ func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeT
Intent: payload.Intent, Intent: payload.Intent,
Lines: lines, Lines: lines,
Applied: applied, Applied: applied,
FxUsed: fx, FxUsed: fxResult,
} }
return resp, nil return resp, nil
} }
@@ -380,24 +341,31 @@ func (s *Service) validateQuoteRequest(req *feesv1.QuoteFeesRequest) error {
if req == nil { if req == nil {
return status.Error(codes.InvalidArgument, "request is required") return status.Error(codes.InvalidArgument, "request is required")
} }
if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetOrganizationRef()) == "" { if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetOrganizationRef()) == "" {
return status.Error(codes.InvalidArgument, "meta.organization_ref is required") return status.Error(codes.InvalidArgument, "meta.organization_ref is required")
} }
if req.GetIntent() == nil { if req.GetIntent() == nil {
return status.Error(codes.InvalidArgument, "intent is required") return status.Error(codes.InvalidArgument, "intent is required")
} }
if req.GetIntent().GetTrigger() == feesv1.Trigger_TRIGGER_UNSPECIFIED { if req.GetIntent().GetTrigger() == feesv1.Trigger_TRIGGER_UNSPECIFIED {
return status.Error(codes.InvalidArgument, "intent.trigger is required") return status.Error(codes.InvalidArgument, "intent.trigger is required")
} }
if req.GetIntent().GetBaseAmount() == nil { if req.GetIntent().GetBaseAmount() == nil {
return status.Error(codes.InvalidArgument, "intent.base_amount is required") return status.Error(codes.InvalidArgument, "intent.base_amount is required")
} }
if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetAmount()) == "" { if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetAmount()) == "" {
return status.Error(codes.InvalidArgument, "intent.base_amount.amount is required") return status.Error(codes.InvalidArgument, "intent.base_amount.amount is required")
} }
if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetCurrency()) == "" { if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetCurrency()) == "" {
return status.Error(codes.InvalidArgument, "intent.base_amount.currency is required") return status.Error(codes.InvalidArgument, "intent.base_amount.currency is required")
} }
return nil return nil
} }
@@ -405,6 +373,7 @@ func (s *Service) validatePrecomputeRequest(req *feesv1.PrecomputeFeesRequest) e
if req == nil { if req == nil {
return status.Error(codes.InvalidArgument, "request is required") return status.Error(codes.InvalidArgument, "request is required")
} }
return s.validateQuoteRequest(&feesv1.QuoteFeesRequest{Meta: req.GetMeta(), Intent: req.GetIntent()}) return s.validateQuoteRequest(&feesv1.QuoteFeesRequest{Meta: req.GetMeta(), Intent: req.GetIntent()})
} }
@@ -413,17 +382,13 @@ func (s *Service) computeQuote(ctx context.Context, orgRef bson.ObjectID, intent
} }
func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID, intent *feesv1.Intent, _ *feesv1.PolicyOverrides, trace *tracev1.TraceContext, now time.Time) ([]*feesv1.DerivedPostingLine, []*feesv1.AppliedRule, *feesv1.FXUsed, error) { func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID, intent *feesv1.Intent, _ *feesv1.PolicyOverrides, trace *tracev1.TraceContext, now time.Time) ([]*feesv1.DerivedPostingLine, []*feesv1.AppliedRule, *feesv1.FXUsed, error) {
bookedAt := now bookedAt := resolvedBookedAt(intent, now)
if intent.GetBookedAt() != nil && intent.GetBookedAt().IsValid() {
bookedAt = intent.GetBookedAt().AsTime() logFields := []zap.Field{zap.Time("booked_at_used", bookedAt)}
if !orgRef.IsZero() {
logFields = append(logFields, mzap.ObjRef("organization_ref", orgRef))
} }
logFields := []zap.Field{
zap.Time("booked_at_used", bookedAt),
}
if !orgRef.IsZero() {
logFields = append(logFields, zap.String("organization_ref", orgRef.Hex()))
}
logFields = append(logFields, logFieldsFromIntent(intent)...) logFields = append(logFields, logFieldsFromIntent(intent)...)
logFields = append(logFields, logFieldsFromTrace(trace)...) logFields = append(logFields, logFieldsFromTrace(trace)...)
logger := s.logger.With(logFields...) logger := s.logger.With(logFields...)
@@ -436,22 +401,13 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes()) plan, rule, err := s.resolver.ResolveFeeRule(ctx, orgPtr, convertTrigger(intent.GetTrigger()), bookedAt, intent.GetAttributes())
if err != nil { if err != nil {
s.logger.Warn("Failed to resolve fee rule", zap.Error(err)) s.logger.Warn("Failed to resolve fee rule", zap.Error(err))
switch {
case errors.Is(err, merrors.ErrNoData): return nil, nil, nil, mapResolveError(err)
return nil, nil, nil, status.Error(codes.NotFound, fmt.Sprintf("fee rule not found: %s", err.Error()))
case errors.Is(err, merrors.ErrDataConflict):
return nil, nil, nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("conflicting fee rules: %s", err.Error()))
case errors.Is(err, storage.ErrConflictingFeePlans):
return nil, nil, nil, status.Error(codes.FailedPrecondition, fmt.Sprintf("conflicting fee plans: %s", err.Error()))
case errors.Is(err, storage.ErrFeePlanNotFound):
return nil, nil, nil, status.Error(codes.NotFound, fmt.Sprintf("fee plan not found: %s", err.Error()))
default:
return nil, nil, nil, status.Error(codes.Internal, fmt.Sprintf("failed to resolve fee rule: %s", err.Error()))
}
} }
originalRules := plan.Rules originalRules := plan.Rules
plan.Rules = []model.FeeRule{*rule} plan.Rules = []model.FeeRule{*rule}
defer func() { defer func() {
plan.Rules = originalRules plan.Rules = originalRules
}() }()
@@ -461,13 +417,116 @@ func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef bson.ObjectID
if errors.Is(calcErr, merrors.ErrInvalidArg) { if errors.Is(calcErr, merrors.ErrInvalidArg) {
return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error()) return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error())
} }
logger.Warn("failed to compute fee quote", zap.Error(calcErr))
logger.Warn("Failed to compute fee quote", zap.Error(calcErr))
return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote") return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote")
} }
return result.Lines, result.Applied, result.FxUsed, nil return result.Lines, result.Applied, result.FxUsed, nil
} }
func resolvedBookedAt(intent *feesv1.Intent, now time.Time) time.Time {
if intent.GetBookedAt() != nil && intent.GetBookedAt().IsValid() {
return intent.GetBookedAt().AsTime()
}
return now
}
func mapResolveError(err error) error {
switch {
case errors.Is(err, merrors.ErrNoData):
return status.Error(codes.NotFound, "fee rule not found: "+err.Error())
case errors.Is(err, merrors.ErrDataConflict):
return status.Error(codes.FailedPrecondition, "conflicting fee rules: "+err.Error())
case errors.Is(err, storage.ErrConflictingFeePlans):
return status.Error(codes.FailedPrecondition, "conflicting fee plans: "+err.Error())
case errors.Is(err, storage.ErrFeePlanNotFound):
return status.Error(codes.NotFound, "fee plan not found: "+err.Error())
default:
return status.Error(codes.Internal, "failed to resolve fee rule: "+err.Error())
}
}
func (s *Service) observePrecomputeFees(logger mlogger.Logger, err error, resp *feesv1.PrecomputeFeesResponse, trigger feesv1.Trigger, start time.Time) {
statusLabel := statusFromError(err)
fxUsed := false
linesCount := 0
appliedCount := 0
var expiresAt time.Time
if err == nil && resp != nil {
fxUsed = resp.GetFxUsed() != nil
linesCount = len(resp.GetLines())
appliedCount = len(resp.GetApplied())
if ts := resp.GetExpiresAt(); ts != nil {
expiresAt = ts.AsTime()
}
}
observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Int("lines", linesCount),
zap.Int("applied_rules", appliedCount),
}
if !expiresAt.IsZero() {
logFields = append(logFields, zap.Time("expires_at", expiresAt))
}
if err != nil {
logger.Warn("PrecomputeFees finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("PrecomputeFees finished", logFields...)
}
func (s *Service) observeValidateFeeToken(logger mlogger.Logger, err error, resp *feesv1.ValidateFeeTokenResponse, trigger feesv1.Trigger, resultReason string, start time.Time) {
statusLabel := statusFromError(err)
fxUsed := false
if err == nil && resp != nil {
if !resp.GetValid() {
statusLabel = "invalid"
}
fxUsed = resp.GetFxUsed() != nil
if resp.GetIntent() != nil {
trigger = resp.GetIntent().GetTrigger()
}
}
observeMetrics("validate", trigger, statusLabel, fxUsed, time.Since(start))
logFields := []zap.Field{
zap.String("status", statusLabel),
zap.Duration("duration", time.Since(start)),
zap.Bool("fx_used", fxUsed),
zap.String("trigger", trigger.String()),
zap.Bool("valid", resp != nil && resp.GetValid()),
}
if resultReason != "" {
logFields = append(logFields, zap.String("reason", resultReason))
}
if err != nil {
logger.Warn("ValidateFeeToken finished", append(logFields, zap.Error(err))...)
return
}
logger.Info("ValidateFeeToken finished", logFields...)
}
type feeQuoteTokenPayload struct { type feeQuoteTokenPayload struct {
OrganizationRef string `json:"organization_ref"` OrganizationRef string `json:"organization_ref"`
Intent *feesv1.Intent `json:"intent"` Intent *feesv1.Intent `json:"intent"`
@@ -480,17 +539,36 @@ func encodeTokenPayload(payload feeQuoteTokenPayload) (string, error) {
if err != nil { if err != nil {
return "", merrors.Internal("fees: failed to serialize token payload") return "", merrors.Internal("fees: failed to serialize token payload")
} }
return base64.StdEncoding.EncodeToString(data), nil return base64.StdEncoding.EncodeToString(data), nil
} }
func decodeTokenPayload(token string) (feeQuoteTokenPayload, error) { func decodeTokenPayload(token string) (feeQuoteTokenPayload, error) {
var payload feeQuoteTokenPayload var payload feeQuoteTokenPayload
data, err := base64.StdEncoding.DecodeString(token) data, err := base64.StdEncoding.DecodeString(token)
if err != nil { if err != nil {
return payload, merrors.InvalidArgument("fees: invalid token encoding") return payload, merrors.InvalidArgument("fees: invalid token encoding")
} }
if err := json.Unmarshal(data, &payload); err != nil { if err := json.Unmarshal(data, &payload); err != nil {
return payload, merrors.InvalidArgument("fees: invalid token payload") return payload, merrors.InvalidArgument("fees: invalid token payload")
} }
return payload, nil return payload, nil
} }
func (s *Service) startDiscoveryAnnouncer() {
if s == nil || s.producer == nil {
return
}
announce := discovery.Announcement{
Service: mservice.BillingFees,
Operations: []string{discovery.OperationFeeCalc},
InvokeURI: s.invokeURI,
Version: appversion.Create().Short(),
}
s.announcer = discovery.NewAnnouncer(s.logger, s.producer, mservice.FeePlans, announce)
s.announcer.Start()
}

View File

@@ -20,7 +20,7 @@ import (
) )
func TestQuoteFees_ComputesDerivedLines(t *testing.T) { func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
t.Helper() t.Parallel()
now := time.Date(2024, 1, 10, 16, 0, 0, 0, time.UTC) now := time.Date(2024, 1, 10, 16, 0, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -93,9 +93,11 @@ func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
if got := line.GetMoney().GetAmount(); got != "3.20" { if got := line.GetMoney().GetAmount(); got != "3.20" {
t.Fatalf("expected fee amount 3.20, got %s", got) t.Fatalf("expected fee amount 3.20, got %s", got)
} }
if line.GetMoney().GetCurrency() != "USD" { if line.GetMoney().GetCurrency() != "USD" {
t.Fatalf("expected currency USD, got %s", line.GetMoney().GetCurrency()) t.Fatalf("expected currency USD, got %s", line.GetMoney().GetCurrency())
} }
if line.GetLedgerAccountRef() != "acct:fees" { if line.GetLedgerAccountRef() != "acct:fees" {
t.Fatalf("unexpected ledger account ref %s", line.GetLedgerAccountRef()) t.Fatalf("unexpected ledger account ref %s", line.GetLedgerAccountRef())
} }
@@ -111,16 +113,18 @@ func TestQuoteFees_ComputesDerivedLines(t *testing.T) {
if applied.GetTaxCode() != "VAT" || applied.GetTaxRate() != "0.20" { if applied.GetTaxCode() != "VAT" || applied.GetTaxRate() != "0.20" {
t.Fatalf("applied rule metadata mismatch: %+v", applied) t.Fatalf("applied rule metadata mismatch: %+v", applied)
} }
if applied.GetRounding() != moneyv1.RoundingMode_ROUND_HALF_UP { if applied.GetRounding() != moneyv1.RoundingMode_ROUND_HALF_UP {
t.Fatalf("expected rounding HALF_UP, got %v", applied.GetRounding()) t.Fatalf("expected rounding HALF_UP, got %v", applied.GetRounding())
} }
if applied.GetParameters()["scale"] != "2" { if applied.GetParameters()["scale"] != "2" {
t.Fatalf("expected parameters to carry metadata scale, got %+v", applied.GetParameters()) t.Fatalf("expected parameters to carry metadata scale, got %+v", applied.GetParameters())
} }
} }
func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) { func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) {
t.Helper() t.Parallel()
now := time.Date(2024, 5, 20, 9, 30, 0, 0, time.UTC) now := time.Date(2024, 5, 20, 9, 30, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -189,20 +193,23 @@ func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QuoteFees returned error: %v", err) t.Fatalf("QuoteFees returned error: %v", err)
} }
if len(resp.GetLines()) != 1 { if len(resp.GetLines()) != 1 {
t.Fatalf("expected only base rule to fire, got %d lines", len(resp.GetLines())) t.Fatalf("expected only base rule to fire, got %d lines", len(resp.GetLines()))
} }
line := resp.GetLines()[0] line := resp.GetLines()[0]
if line.GetLedgerAccountRef() != "acct:base" { if line.GetLedgerAccountRef() != "acct:base" {
t.Fatalf("expected base rule to apply, got %s", line.GetLedgerAccountRef()) t.Fatalf("expected base rule to apply, got %s", line.GetLedgerAccountRef())
} }
if line.GetMoney().GetAmount() != "5.00" { if line.GetMoney().GetAmount() != "5.00" {
t.Fatalf("expected 5.00 amount, got %s", line.GetMoney().GetAmount()) t.Fatalf("expected 5.00 amount, got %s", line.GetMoney().GetAmount())
} }
} }
func TestQuoteFees_RoundingDown(t *testing.T) { func TestQuoteFees_RoundingDown(t *testing.T) {
t.Helper() t.Parallel()
now := time.Date(2024, 3, 15, 12, 0, 0, 0, time.UTC) now := time.Date(2024, 3, 15, 12, 0, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -249,16 +256,18 @@ func TestQuoteFees_RoundingDown(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QuoteFees returned error: %v", err) t.Fatalf("QuoteFees returned error: %v", err)
} }
if len(resp.GetLines()) != 1 { if len(resp.GetLines()) != 1 {
t.Fatalf("expected single derived line, got %d", len(resp.GetLines())) t.Fatalf("expected single derived line, got %d", len(resp.GetLines()))
} }
if resp.GetLines()[0].GetMoney().GetAmount() != "0.01" { if resp.GetLines()[0].GetMoney().GetAmount() != "0.01" {
t.Fatalf("expected rounding down to 0.01, got %s", resp.GetLines()[0].GetMoney().GetAmount()) t.Fatalf("expected rounding down to 0.01, got %s", resp.GetLines()[0].GetMoney().GetAmount())
} }
} }
func TestQuoteFees_UsesInjectedCalculator(t *testing.T) { func TestQuoteFees_UsesInjectedCalculator(t *testing.T) {
t.Helper() t.Parallel()
now := time.Date(2024, 6, 1, 8, 0, 0, 0, time.UTC) now := time.Date(2024, 6, 1, 8, 0, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -316,22 +325,26 @@ func TestQuoteFees_UsesInjectedCalculator(t *testing.T) {
if err != nil { if err != nil {
t.Fatalf("QuoteFees returned error: %v", err) t.Fatalf("QuoteFees returned error: %v", err)
} }
if !calc.called { if !calc.called {
t.Fatalf("expected calculator to be invoked") t.Fatalf("expected calculator to be invoked")
} }
if calc.gotPlan != plan { if calc.gotPlan != plan {
t.Fatalf("expected calculator to receive plan pointer") t.Fatalf("expected calculator to receive plan pointer")
} }
if len(resp.GetLines()) != len(result.Lines) { if len(resp.GetLines()) != len(result.Lines) {
t.Fatalf("expected %d lines, got %d", len(result.Lines), len(resp.GetLines())) t.Fatalf("expected %d lines, got %d", len(result.Lines), len(resp.GetLines()))
} }
if resp.GetLines()[0].GetLedgerAccountRef() != "acct:stub" { if resp.GetLines()[0].GetLedgerAccountRef() != "acct:stub" {
t.Fatalf("unexpected ledger account in response: %s", resp.GetLines()[0].GetLedgerAccountRef()) t.Fatalf("unexpected ledger account in response: %s", resp.GetLines()[0].GetLedgerAccountRef())
} }
} }
func TestQuoteFees_PopulatesFxUsed(t *testing.T) { func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
t.Helper() t.Parallel()
now := time.Date(2024, 7, 1, 9, 30, 0, 0, time.UTC) now := time.Date(2024, 7, 1, 9, 30, 0, 0, time.UTC)
orgRef := bson.NewObjectID() orgRef := bson.NewObjectID()
@@ -356,7 +369,7 @@ func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
plan.OrganizationRef = &orgRef plan.OrganizationRef = &orgRef
fakeOracle := &oracleclient.Fake{ fakeOracle := &oracleclient.Fake{
LatestRateFn: func(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) { LatestRateFn: func(_ context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) {
return &oracleclient.RateSnapshot{ return &oracleclient.RateSnapshot{
Pair: req.Pair, Pair: req.Pair,
Mid: "1.2300", Mid: "1.2300",
@@ -399,10 +412,12 @@ func TestQuoteFees_PopulatesFxUsed(t *testing.T) {
if resp.GetFxUsed() == nil { if resp.GetFxUsed() == nil {
t.Fatalf("expected FxUsed to be populated") t.Fatalf("expected FxUsed to be populated")
} }
fx := resp.GetFxUsed() fx := resp.GetFxUsed()
if fx.GetProvider() != "TestProvider" || fx.GetRate().GetValue() != "1.2300" { if fx.GetProvider() != "TestProvider" || fx.GetRate().GetValue() != "1.2300" {
t.Fatalf("unexpected FxUsed payload: %+v", fx) t.Fatalf("unexpected FxUsed payload: %+v", fx)
} }
if fx.GetPair().GetBase() != "USD" || fx.GetPair().GetQuote() != "EUR" { if fx.GetPair().GetBase() != "USD" || fx.GetPair().GetQuote() != "EUR" {
t.Fatalf("unexpected currency pair: %+v", fx.GetPair()) t.Fatalf("unexpected currency pair: %+v", fx.GetPair())
} }
@@ -437,49 +452,59 @@ func (s *stubPlansStore) Get(context.Context, bson.ObjectID) (*model.FeePlan, er
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
func (s *stubPlansStore) GetActivePlan(_ context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (s *stubPlansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
if !orgRef.IsZero() { if !orgRef.IsZero() {
if plan, err := s.FindActiveOrgPlan(context.Background(), orgRef, at); err == nil { if plan, err := s.FindActiveOrgPlan(ctx, orgRef, asOf); err == nil {
return plan, nil return plan, nil
} else if !errors.Is(err, storage.ErrFeePlanNotFound) { } else if !errors.Is(err, storage.ErrFeePlanNotFound) {
return nil, err return nil, err
} }
} }
return s.FindActiveGlobalPlan(context.Background(), at)
return s.FindActiveGlobalPlan(ctx, asOf)
} }
func (s *stubPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (s *stubPlansStore) FindActiveOrgPlan(_ context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
if s.plan == nil { if s.plan == nil {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if (s.plan.OrganizationRef != nil) && (*s.plan.OrganizationRef != orgRef) { if (s.plan.OrganizationRef != nil) && (*s.plan.OrganizationRef != orgRef) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if !s.plan.Active { if !s.plan.Active {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.plan.EffectiveFrom.After(at) {
if s.plan.EffectiveFrom.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.plan.EffectiveTo != nil && !s.plan.EffectiveTo.After(at) {
if s.plan.EffectiveTo != nil && !s.plan.EffectiveTo.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return s.plan, nil return s.plan, nil
} }
func (s *stubPlansStore) FindActiveGlobalPlan(_ context.Context, at time.Time) (*model.FeePlan, error) { func (s *stubPlansStore) FindActiveGlobalPlan(_ context.Context, asOf time.Time) (*model.FeePlan, error) {
if s.globalPlan == nil { if s.globalPlan == nil {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if !s.globalPlan.Active { if !s.globalPlan.Active {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.globalPlan.EffectiveFrom.After(at) {
if s.globalPlan.EffectiveFrom.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if s.globalPlan.EffectiveTo != nil && !s.globalPlan.EffectiveTo.After(at) {
if s.globalPlan.EffectiveTo != nil && !s.globalPlan.EffectiveTo.After(asOf) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return s.globalPlan, nil return s.globalPlan, nil
} }
@@ -509,8 +534,10 @@ func (s *stubCalculator) Compute(_ context.Context, plan *model.FeePlan, _ *fees
s.called = true s.called = true
s.gotPlan = plan s.gotPlan = plan
s.bookedAt = bookedAt s.bookedAt = bookedAt
if s.err != nil { if s.err != nil {
return nil, s.err return nil, s.err
} }
return s.result, nil return s.result, nil
} }

View File

@@ -7,6 +7,8 @@ import (
func convertTrigger(trigger feesv1.Trigger) model.Trigger { func convertTrigger(trigger feesv1.Trigger) model.Trigger {
switch trigger { switch trigger {
case feesv1.Trigger_TRIGGER_UNSPECIFIED:
return model.TriggerUnspecified
case feesv1.Trigger_TRIGGER_CAPTURE: case feesv1.Trigger_TRIGGER_CAPTURE:
return model.TriggerCapture return model.TriggerCapture
case feesv1.Trigger_TRIGGER_REFUND: case feesv1.Trigger_TRIGGER_REFUND:

View File

@@ -28,12 +28,13 @@ const (
type FeePlan struct { type FeePlan struct {
storable.Base `bson:",inline" json:",inline"` storable.Base `bson:",inline" json:",inline"`
model.Describable `bson:",inline" json:",inline"` model.Describable `bson:",inline" json:",inline"`
OrganizationRef *bson.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
Active bool `bson:"active" json:"active"` OrganizationRef *bson.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"`
EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"` Active bool `bson:"active" json:"active"`
EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"`
Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"` EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
} }
// Collection implements storable.Storable. // Collection implements storable.Storable.
@@ -43,21 +44,21 @@ func (*FeePlan) Collection() string {
// FeeRule represents a single pricing rule within a plan. // FeeRule represents a single pricing rule within a plan.
type FeeRule struct { type FeeRule struct {
RuleID string `bson:"ruleId" json:"ruleId"` RuleID string `bson:"ruleId" json:"ruleId"`
Trigger Trigger `bson:"trigger" json:"trigger"` Trigger Trigger `bson:"trigger" json:"trigger"`
Priority int `bson:"priority" json:"priority"` Priority int `bson:"priority" json:"priority"`
Percentage string `bson:"percentage,omitempty" json:"percentage,omitempty"` Percentage string `bson:"percentage,omitempty" json:"percentage,omitempty"`
FixedAmount string `bson:"fixedAmount,omitempty" json:"fixedAmount,omitempty"` FixedAmount string `bson:"fixedAmount,omitempty" json:"fixedAmount,omitempty"`
Currency string `bson:"currency,omitempty" json:"currency,omitempty"` Currency string `bson:"currency,omitempty" json:"currency,omitempty"`
MinimumAmount string `bson:"minimumAmount,omitempty" json:"minimumAmount,omitempty"` MinimumAmount string `bson:"minimumAmount,omitempty" json:"minimumAmount,omitempty"`
MaximumAmount string `bson:"maximumAmount,omitempty" json:"maximumAmount,omitempty"` MaximumAmount string `bson:"maximumAmount,omitempty" json:"maximumAmount,omitempty"`
AppliesTo map[string]string `bson:"appliesTo,omitempty" json:"appliesTo,omitempty"` AppliesTo map[string]string `bson:"appliesTo,omitempty" json:"appliesTo,omitempty"`
Formula string `bson:"formula,omitempty" json:"formula,omitempty"` Formula string `bson:"formula,omitempty" json:"formula,omitempty"`
Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"`
LedgerAccountRef string `bson:"ledgerAccountRef,omitempty" json:"ledgerAccountRef,omitempty"` LedgerAccountRef string `bson:"ledgerAccountRef,omitempty" json:"ledgerAccountRef,omitempty"`
LineType string `bson:"lineType,omitempty" json:"lineType,omitempty"` LineType string `bson:"lineType,omitempty" json:"lineType,omitempty"`
EntrySide string `bson:"entrySide,omitempty" json:"entrySide,omitempty"` EntrySide string `bson:"entrySide,omitempty" json:"entrySide,omitempty"`
Rounding string `bson:"rounding,omitempty" json:"rounding,omitempty"` Rounding string `bson:"rounding,omitempty" json:"rounding,omitempty"`
EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"` EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"`
EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"`
} }

View File

@@ -43,18 +43,22 @@ func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) {
defer cancel() defer cancel()
if err := result.Ping(ctx); err != nil { if err := result.Ping(ctx); err != nil {
result.logger.Error("mongo ping failed during store init", zap.Error(err)) result.logger.Error("Mongo ping failed during store init", zap.Error(err))
return nil, err return nil, err
} }
plansStore, err := store.NewPlans(result.logger, database) plansStore, err := store.NewPlans(result.logger, database)
if err != nil { if err != nil {
result.logger.Error("failed to initialise plans store", zap.Error(err)) result.logger.Error("Failed to initialise plans store", zap.Error(err))
return nil, err return nil, err
} }
result.plans = plansStore result.plans = plansStore
result.logger.Info("Billing fees MongoDB storage initialised") result.logger.Info("Billing fees MongoDB storage initialised")
return result, nil return result, nil
} }

View File

@@ -28,6 +28,8 @@ type plansStore struct {
repo repository.Repository repo repository.Repository
} }
const maxActivePlanResults = 2
// NewPlans constructs a Mongo-backed PlansStore. // NewPlans constructs a Mongo-backed PlansStore.
func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, error) { func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, error) {
repo := repository.CreateMongoRepository(db, mservice.FeePlans) repo := repository.CreateMongoRepository(db, mservice.FeePlans)
@@ -40,7 +42,8 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
}, },
} }
if err := repo.CreateIndex(orgIndex); err != nil { if err := repo.CreateIndex(orgIndex); err != nil {
logger.Error("failed to ensure fee plan organization index", zap.Error(err)) logger.Error("Failed to ensure fee plan organization index", zap.Error(err))
return nil, err return nil, err
} }
@@ -53,7 +56,8 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
Unique: true, Unique: true,
} }
if err := repo.CreateIndex(uniqueIndex); err != nil { if err := repo.CreateIndex(uniqueIndex); err != nil {
logger.Error("failed to ensure fee plan uniqueness index", zap.Error(err)) logger.Error("Failed to ensure fee plan uniqueness index", zap.Error(err))
return nil, err return nil, err
} }
@@ -67,7 +71,7 @@ func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, er
}, },
} }
if err := repo.CreateIndex(activeIndex); err != nil { if err := repo.CreateIndex(activeIndex); err != nil {
logger.Warn("failed to ensure fee plan active index", zap.Error(err)) logger.Warn("Failed to ensure fee plan active index", zap.Error(err))
} }
return &plansStore{ return &plansStore{
@@ -80,6 +84,7 @@ func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error {
if err := validatePlan(plan); err != nil { if err := validatePlan(plan); err != nil {
return err return err
} }
if err := p.ensureNoOverlap(ctx, plan); err != nil { if err := p.ensureNoOverlap(ctx, plan); err != nil {
return err return err
} }
@@ -88,9 +93,12 @@ func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error {
if errors.Is(err, merrors.ErrDataConflict) { if errors.Is(err, merrors.ErrDataConflict) {
return storage.ErrDuplicateFeePlan return storage.ErrDuplicateFeePlan
} }
p.logger.Warn("failed to create fee plan", zap.Error(err))
p.logger.Warn("Failed to create fee plan", zap.Error(err))
return err return err
} }
return nil return nil
} }
@@ -98,17 +106,21 @@ func (p *plansStore) Update(ctx context.Context, plan *model.FeePlan) error {
if plan == nil || plan.GetID() == nil || plan.GetID().IsZero() { if plan == nil || plan.GetID() == nil || plan.GetID().IsZero() {
return merrors.InvalidArgument("plansStore: invalid fee plan reference") return merrors.InvalidArgument("plansStore: invalid fee plan reference")
} }
if err := validatePlan(plan); err != nil { if err := validatePlan(plan); err != nil {
return err return err
} }
if err := p.ensureNoOverlap(ctx, plan); err != nil { if err := p.ensureNoOverlap(ctx, plan); err != nil {
return err return err
} }
if err := p.repo.Update(ctx, plan); err != nil { if err := p.repo.Update(ctx, plan); err != nil {
p.logger.Warn("failed to update fee plan", zap.Error(err)) p.logger.Warn("Failed to update fee plan", zap.Error(err))
return err return err
} }
return nil return nil
} }
@@ -116,72 +128,83 @@ func (p *plansStore) Get(ctx context.Context, planRef bson.ObjectID) (*model.Fee
if planRef.IsZero() { if planRef.IsZero() {
return nil, merrors.InvalidArgument("plansStore: zero plan reference") return nil, merrors.InvalidArgument("plansStore: zero plan reference")
} }
result := &model.FeePlan{} result := &model.FeePlan{}
if err := p.repo.Get(ctx, planRef, result); err != nil { if err := p.repo.Get(ctx, planRef, result); err != nil {
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return nil, err return nil, err
} }
return result, nil return result, nil
} }
func (p *plansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (p *plansStore) GetActivePlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
// Compatibility shim: prefer org plan, fall back to global; allow zero org to mean global. // Compatibility shim: prefer org plan, fall back to global; allow zero org to mean global.
if orgRef.IsZero() { if orgRef.IsZero() {
return p.FindActiveGlobalPlan(ctx, at) return p.FindActiveGlobalPlan(ctx, asOf)
} }
plan, err := p.FindActiveOrgPlan(ctx, orgRef, at) plan, err := p.FindActiveOrgPlan(ctx, orgRef, asOf)
if err == nil { if err == nil {
return plan, nil return plan, nil
} }
if errors.Is(err, storage.ErrFeePlanNotFound) { if errors.Is(err, storage.ErrFeePlanNotFound) {
return p.FindActiveGlobalPlan(ctx, at) return p.FindActiveGlobalPlan(ctx, asOf)
} }
return nil, err return nil, err
} }
func (p *plansStore) FindActiveOrgPlan(ctx context.Context, orgRef bson.ObjectID, at time.Time) (*model.FeePlan, error) { func (p *plansStore) FindActiveOrgPlan(ctx context.Context, orgRef bson.ObjectID, asOf time.Time) (*model.FeePlan, error) {
if orgRef.IsZero() { if orgRef.IsZero() {
return nil, merrors.InvalidArgument("plansStore: zero organization reference") return nil, merrors.InvalidArgument("plansStore: zero organization reference")
} }
query := repository.Query().Filter(repository.OrgField(), orgRef) query := repository.Query().Filter(repository.OrgField(), orgRef)
return p.findActivePlan(ctx, query, at)
return p.findActivePlan(ctx, query, asOf)
} }
func (p *plansStore) FindActiveGlobalPlan(ctx context.Context, at time.Time) (*model.FeePlan, error) { func (p *plansStore) FindActiveGlobalPlan(ctx context.Context, asOf time.Time) (*model.FeePlan, error) {
globalQuery := repository.Query().Or( globalQuery := repository.Query().Or(
repository.Exists(repository.OrgField(), false), repository.Exists(repository.OrgField(), false),
repository.Query().Filter(repository.OrgField(), nil), repository.Query().Filter(repository.OrgField(), nil),
) )
return p.findActivePlan(ctx, globalQuery, at)
return p.findActivePlan(ctx, globalQuery, asOf)
} }
var _ storage.PlansStore = (*plansStore)(nil) var _ storage.PlansStore = (*plansStore)(nil)
func (p *plansStore) findActivePlan(ctx context.Context, orgQuery builder.Query, at time.Time) (*model.FeePlan, error) { func (p *plansStore) findActivePlan(ctx context.Context, orgQuery builder.Query, asOf time.Time) (*model.FeePlan, error) {
limit := int64(2) limit := int64(maxActivePlanResults)
query := orgQuery. query := orgQuery.
Filter(repository.Field("active"), true). Filter(repository.Field("active"), true).
Comparison(repository.Field("effectiveFrom"), builder.Lte, at). Comparison(repository.Field("effectiveFrom"), builder.Lte, asOf).
Sort(repository.Field("effectiveFrom"), false). Sort(repository.Field("effectiveFrom"), false).
Limit(&limit) Limit(&limit)
query = query.And( query = query.And(
repository.Query().Or( repository.Query().Or(
repository.Query().Filter(repository.Field("effectiveTo"), nil), repository.Query().Filter(repository.Field("effectiveTo"), nil),
repository.Query().Comparison(repository.Field("effectiveTo"), builder.Gte, at), repository.Query().Comparison(repository.Field("effectiveTo"), builder.Gte, asOf),
), ),
) )
var plans []*model.FeePlan var plans []*model.FeePlan
decoder := func(cursor *mongo.Cursor) error { decoder := func(cursor *mongo.Cursor) error {
target := &model.FeePlan{} target := &model.FeePlan{}
if err := cursor.Decode(target); err != nil { if err := cursor.Decode(target); err != nil {
return err return err
} }
plans = append(plans, target) plans = append(plans, target)
return nil return nil
} }
@@ -189,15 +212,18 @@ func (p *plansStore) findActivePlan(ctx context.Context, orgQuery builder.Query,
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
return nil, err return nil, err
} }
if len(plans) == 0 { if len(plans) == 0 {
return nil, storage.ErrFeePlanNotFound return nil, storage.ErrFeePlanNotFound
} }
if len(plans) > 1 { if len(plans) > 1 {
return nil, storage.ErrConflictingFeePlans return nil, storage.ErrConflictingFeePlans
} }
return plans[0], nil return plans[0], nil
} }
@@ -205,44 +231,61 @@ func validatePlan(plan *model.FeePlan) error {
if plan == nil { if plan == nil {
return merrors.InvalidArgument("plansStore: nil fee plan") return merrors.InvalidArgument("plansStore: nil fee plan")
} }
if len(plan.Rules) == 0 { if len(plan.Rules) == 0 {
return merrors.InvalidArgument("plansStore: fee plan must contain at least one rule") return merrors.InvalidArgument("plansStore: fee plan must contain at least one rule")
} }
if plan.Active && plan.EffectiveTo != nil && plan.EffectiveTo.Before(plan.EffectiveFrom) { if plan.Active && plan.EffectiveTo != nil && plan.EffectiveTo.Before(plan.EffectiveFrom) {
return merrors.InvalidArgument("plansStore: effectiveTo cannot be before effectiveFrom") return merrors.InvalidArgument("plansStore: effectiveTo cannot be before effectiveFrom")
} }
// Ensure unique priority per (trigger, appliesTo) combination. // Ensure unique priority per (trigger, appliesTo) combination.
seen := make(map[string]struct{}) seen := make(map[string]struct{})
for _, rule := range plan.Rules { for _, rule := range plan.Rules {
if strings.TrimSpace(rule.Percentage) != "" { if err := validateRule(rule); err != nil {
if _, err := dmath.RatFromString(rule.Percentage); err != nil { return err
return merrors.InvalidArgument("plansStore: invalid rule percentage")
}
}
if strings.TrimSpace(rule.FixedAmount) != "" {
if _, err := dmath.RatFromString(rule.FixedAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule fixed amount")
}
}
if strings.TrimSpace(rule.MinimumAmount) != "" {
if _, err := dmath.RatFromString(rule.MinimumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule minimum amount")
}
}
if strings.TrimSpace(rule.MaximumAmount) != "" {
if _, err := dmath.RatFromString(rule.MaximumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule maximum amount")
}
} }
appliesKey := normalizeAppliesTo(rule.AppliesTo) appliesKey := normalizeAppliesTo(rule.AppliesTo)
priorityKey := fmt.Sprintf("%s|%d|%s", rule.Trigger, rule.Priority, appliesKey) priorityKey := fmt.Sprintf("%s|%d|%s", rule.Trigger, rule.Priority, appliesKey)
if _, ok := seen[priorityKey]; ok { if _, ok := seen[priorityKey]; ok {
return merrors.InvalidArgument("plansStore: duplicate priority for trigger/appliesTo") return merrors.InvalidArgument("plansStore: duplicate priority for trigger/appliesTo")
} }
seen[priorityKey] = struct{}{} seen[priorityKey] = struct{}{}
} }
return nil
}
func validateRule(rule model.FeeRule) error {
if strings.TrimSpace(rule.Percentage) != "" {
if _, err := dmath.RatFromString(rule.Percentage); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule percentage")
}
}
if strings.TrimSpace(rule.FixedAmount) != "" {
if _, err := dmath.RatFromString(rule.FixedAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule fixed amount")
}
}
if strings.TrimSpace(rule.MinimumAmount) != "" {
if _, err := dmath.RatFromString(rule.MinimumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule minimum amount")
}
}
if strings.TrimSpace(rule.MaximumAmount) != "" {
if _, err := dmath.RatFromString(rule.MaximumAmount); err != nil {
return merrors.InvalidArgument("plansStore: invalid rule maximum amount")
}
}
return nil return nil
} }
@@ -250,15 +293,19 @@ func normalizeAppliesTo(applies map[string]string) string {
if len(applies) == 0 { if len(applies) == 0 {
return "" return ""
} }
keys := make([]string, 0, len(applies)) keys := make([]string, 0, len(applies))
for k := range applies { for k := range applies {
keys = append(keys, k) keys = append(keys, k)
} }
sort.Strings(keys) sort.Strings(keys)
parts := make([]string, 0, len(keys)) parts := make([]string, 0, len(keys))
for _, k := range keys { for _, k := range keys {
parts = append(parts, k+"="+normalizeAppliesToValue(applies[k])) parts = append(parts, k+"="+normalizeAppliesToValue(applies[k]))
} }
return strings.Join(parts, ",") return strings.Join(parts, ",")
} }
@@ -272,28 +319,37 @@ func normalizeAppliesToValue(value string) string {
seen := make(map[string]struct{}, len(values)) seen := make(map[string]struct{}, len(values))
normalized := make([]string, 0, len(values)) normalized := make([]string, 0, len(values))
hasWildcard := false hasWildcard := false
for _, value := range values { for _, value := range values {
value = strings.TrimSpace(value) value = strings.TrimSpace(value)
if value == "" { if value == "" {
continue continue
} }
if value == "*" { if value == "*" {
hasWildcard = true hasWildcard = true
continue continue
} }
if _, ok := seen[value]; ok { if _, ok := seen[value]; ok {
continue continue
} }
seen[value] = struct{}{} seen[value] = struct{}{}
normalized = append(normalized, value) normalized = append(normalized, value)
} }
if hasWildcard { if hasWildcard {
return "*" return "*"
} }
if len(normalized) == 0 { if len(normalized) == 0 {
return "" return ""
} }
sort.Strings(normalized) sort.Strings(normalized)
return strings.Join(normalized, ",") return strings.Join(normalized, ",")
} }
@@ -302,7 +358,7 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
return nil return nil
} }
orgQuery := repository.Query() var orgQuery builder.Query
if plan.OrganizationRef.IsZero() { if plan.OrganizationRef.IsZero() {
orgQuery = repository.Query().Or( orgQuery = repository.Query().Or(
repository.Exists(repository.OrgField(), false), repository.Exists(repository.OrgField(), false),
@@ -314,6 +370,7 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
maxTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC) maxTime := time.Date(9999, 12, 31, 23, 59, 59, 0, time.UTC)
newFrom := plan.EffectiveFrom newFrom := plan.EffectiveFrom
newTo := maxTime newTo := maxTime
if plan.EffectiveTo != nil { if plan.EffectiveTo != nil {
newTo = *plan.EffectiveTo newTo = *plan.EffectiveTo
@@ -335,8 +392,10 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
query = query.Limit(&limit) query = query.Limit(&limit)
var overlapFound bool var overlapFound bool
decoder := func(cursor *mongo.Cursor) error {
decoder := func(_ *mongo.Cursor) error {
overlapFound = true overlapFound = true
return nil return nil
} }
@@ -344,10 +403,13 @@ func (p *plansStore) ensureNoOverlap(ctx context.Context, plan *model.FeePlan) e
if errors.Is(err, merrors.ErrNoData) { if errors.Is(err, merrors.ErrNoData) {
return nil return nil
} }
return err return err
} }
if overlapFound { if overlapFound {
return storage.ErrConflictingFeePlans return storage.ErrConflictingFeePlans
} }
return nil return nil
} }

196
api/discovery/.golangci.yml Normal file
View File

@@ -0,0 +1,196 @@
# See the dedicated "version" documentation section.
version: "2"
linters:
# Default set of linters.
# 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:
- arangolint
- asasalint
- asciicheck
- bidichk
- bodyclose
- canonicalheader
- containedctx
- contextcheck
- copyloopvar
- cyclop
- decorder
- dogsled
- dupl
- dupword
- durationcheck
- embeddedstructfieldcheck
- err113
- errcheck
- errchkjson
- errname
- errorlint
- exhaustive
- exptostd
- fatcontext
- forbidigo
- forcetypeassert
- funcorder
- funlen
- ginkgolinter
- gocheckcompilerdirectives
- gochecknoglobals
- gochecknoinits
- gochecksumtype
- gocognit
- goconst
- gocritic
- gocyclo
- godoclint
- godot
- godox
- goheader
- gomodguard
- goprintffuncname
- gosec
- gosmopolitan
- govet
- grouper
- iface
- importas
- inamedparam
- ineffassign
- interfacebloat
- intrange
- iotamixing
- ireturn
- lll
- loggercheck
- maintidx
- makezero
- mirror
- misspell
- mnd
- modernize
- musttag
- nakedret
- nestif
- nilerr
- nilnesserr
- nilnil
- nlreturn
- noctx
- noinlineerr
- nolintlint
- nonamedreturns
- nosprintfhostport
- paralleltest
- perfsprint
- prealloc
- predeclared
- promlinter
- protogetter
- reassign
- recvcheck
- revive
- rowserrcheck
- sloglint
- spancheck
- sqlclosecheck
- staticcheck
- tagalign
- tagliatelle
- testableexamples
- testifylint
- testpackage
- thelper
- tparallel
- unconvert
- unparam
- unqueryvet
- unused
- usestdlibvars
- usetesting
- varnamelen
- wastedassign
- whitespace
- wsl_v5
- zerologlint
# Disable specific linters.
disable:
- depguard
- exhaustruct
- gochecknoglobals
- gomoddirectives
- wsl
- wrapcheck
# All available settings of specific linters.
# See the dedicated "linters.settings" documentation section.
settings:
wsl_v5:
allow-first-in-block: true
allow-whole-block: false
branch-max-lines: 2
# Defines a set of rules to ignore issues.
# It does not skip the analysis, and so does not ignore "typecheck" errors.
exclusions:
# Mode of the generated files analysis.
#
# - `strict`: sources are excluded by strictly following the Go generated file convention.
# Source files that have lines matching only the following regular expression will be excluded: `^// Code generated .* DO NOT EDIT\.$`
# This line must appear before the first non-comment, non-blank text in the file.
# 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

@@ -1,11 +1,11 @@
module github.com/tech/sendico/discovery module github.com/tech/sendico/discovery
go 1.25.6 go 1.25.7
replace github.com/tech/sendico/pkg => ../pkg replace github.com/tech/sendico/pkg => ../pkg
require ( require (
github.com/go-chi/chi/v5 v5.2.4 github.com/go-chi/chi/v5 v5.2.5
github.com/prometheus/client_golang v1.23.2 github.com/prometheus/client_golang v1.23.2
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
@@ -20,17 +20,17 @@ require (
github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect
github.com/cespare/xxhash/v2 v2.3.0 // indirect github.com/cespare/xxhash/v2 v2.3.0 // indirect
github.com/google/uuid v1.6.0 // indirect github.com/google/uuid v1.6.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/klauspost/compress v1.18.4 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/mapstructure v1.5.0 // indirect github.com/mitchellh/mapstructure v1.5.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -38,12 +38,12 @@ require (
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.3 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/net v0.49.0 // indirect golang.org/x/net v0.51.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
google.golang.org/grpc v1.78.0 // indirect google.golang.org/grpc v1.79.1 // indirect
google.golang.org/protobuf v1.36.11 // indirect google.golang.org/protobuf v1.36.11 // indirect
) )

View File

@@ -38,8 +38,8 @@ github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@@ -57,8 +57,8 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk= github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY= github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
@@ -91,8 +91,8 @@ github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -113,8 +113,8 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ= github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog= github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
@@ -152,16 +152,16 @@ 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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
@@ -172,15 +172,15 @@ go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0=
go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8= go.yaml.in/yaml/v2 v2.4.3/go.mod h1:zSxWcmIDjOzPXpjlTTbAsKokqkDNAVtZO0WOMiT90s8=
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4=
@@ -191,16 +191,16 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
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-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/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

@@ -1,3 +1,4 @@
// Package appversion exposes build-time version information for the discovery service.
package appversion package appversion
import ( import (
@@ -14,8 +15,9 @@ var (
BuildDate string BuildDate string
) )
func Create() version.Printer { // Create returns a version printer populated with the compile-time build metadata.
vi := version.Info{ func Create() version.Printer { //nolint:ireturn // factory returns interface by design
info := version.Info{
Program: "Sendico Discovery Service", Program: "Sendico Discovery Service",
Revision: Revision, Revision: Revision,
Branch: Branch, Branch: Branch,
@@ -23,5 +25,6 @@ func Create() version.Printer {
BuildDate: BuildDate, BuildDate: BuildDate,
Version: Version, Version: Version,
} }
return vf.Create(&vi)
return vf.Create(&info)
} }

View File

@@ -1,3 +1,4 @@
// Package serverimp contains the concrete discovery server implementation.
package serverimp package serverimp
import ( import (
@@ -10,7 +11,10 @@ import (
"gopkg.in/yaml.v3" "gopkg.in/yaml.v3"
) )
const defaultMetricsAddress = ":9405" const (
defaultMetricsAddress = ":9405"
defaultShutdownTimeoutSeconds = 15
)
type config struct { type config struct {
Runtime *grpcapp.RuntimeConfig `yaml:"runtime"` Runtime *grpcapp.RuntimeConfig `yaml:"runtime"`
@@ -24,24 +28,28 @@ type metricsConfig struct {
} }
type registryConfig struct { type registryConfig struct {
KVTTLSeconds *int `yaml:"kv_ttl_seconds"` KVTTLSeconds *int `yaml:"kv_ttl_seconds"` //nolint:tagliatelle // matches config file format
} }
func (i *Imp) loadConfig() (*config, error) { func (i *Imp) loadConfig() (*config, error) {
data, err := os.ReadFile(i.file) data, err := os.ReadFile(i.file)
if err != nil { if err != nil {
i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err))
return nil, err
return nil, err //nolint:wrapcheck
} }
cfg := &config{} cfg := &config{}
if err := yaml.Unmarshal(data, cfg); err != nil {
err = yaml.Unmarshal(data, cfg)
if err != nil {
i.logger.Error("Failed to parse configuration", zap.Error(err)) i.logger.Error("Failed to parse configuration", zap.Error(err))
return nil, err
return nil, err //nolint:wrapcheck
} }
if cfg.Runtime == nil { if cfg.Runtime == nil {
cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: defaultShutdownTimeoutSeconds}
} }
if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" { if cfg.Metrics != nil && strings.TrimSpace(cfg.Metrics.Address) == "" {

View File

@@ -14,43 +14,51 @@ import (
func (i *Imp) startDiscovery(cfg *config) error { func (i *Imp) startDiscovery(cfg *config) error {
if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" { if cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" {
//nolint:wrapcheck
return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging") return merrors.InvalidArgument("discovery service: messaging configuration is required", "messaging")
} }
broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging) broker, err := msg.CreateMessagingBroker(i.logger.Named("discovery_bus"), cfg.Messaging)
if err != nil { if err != nil {
return err return err //nolint:wrapcheck
} }
i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver))) i.logger.Info("Discovery messaging broker ready", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker) producer := msgproducer.NewProducer(i.logger.Named("discovery_producer"), broker)
registry := discovery.NewRegistry() registry := discovery.NewRegistry()
var registryOpts []discovery.RegistryOption var registryOpts []discovery.RegistryOption
if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil { if cfg.Registry != nil && cfg.Registry.KVTTLSeconds != nil {
ttlSeconds := *cfg.Registry.KVTTLSeconds ttlSeconds := *cfg.Registry.KVTTLSeconds
if ttlSeconds < 0 { if ttlSeconds < 0 {
i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds)) i.logger.Warn("Discovery registry TTL is negative, disabling TTL", zap.Int("ttl_seconds", ttlSeconds))
ttlSeconds = 0 ttlSeconds = 0
} }
registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second)) registryOpts = append(registryOpts, discovery.WithRegistryKVTTL(time.Duration(ttlSeconds)*time.Second))
} }
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, string(mservice.Discovery), registryOpts...)
svc, err := discovery.NewRegistryService(i.logger, broker, producer, registry, mservice.Discovery, registryOpts...)
if err != nil { if err != nil {
return err return err //nolint:wrapcheck
} }
svc.Start() svc.Start()
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.lookup"}, Operations: []string{discovery.OperationDiscoveryLookup},
Version: appversion.Create().Short(), Version: appversion.Create().Short(),
} }
i.announcer = discovery.NewAnnouncer(i.logger, producer, string(mservice.Discovery), announce) i.announcer = discovery.NewAnnouncer(i.logger, producer, mservice.Discovery, announce)
i.announcer.Start() i.announcer.Start()
i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver))) i.logger.Info("Discovery registry service started", zap.String("messaging_driver", string(cfg.Messaging.Driver)))
return nil return nil
} }
@@ -58,10 +66,12 @@ func (i *Imp) stopDiscovery() {
if i == nil { if i == nil {
return return
} }
if i.announcer != nil { if i.announcer != nil {
i.announcer.Stop() i.announcer.Stop()
i.announcer = nil i.announcer = nil
} }
if i.registrySvc != nil { if i.registrySvc != nil {
i.registrySvc.Stop() i.registrySvc.Stop()
i.registrySvc = nil i.registrySvc = nil

View File

@@ -15,22 +15,30 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const readHeaderTimeout = 5 * time.Second
func (i *Imp) startMetrics(cfg *metricsConfig) { func (i *Imp) startMetrics(cfg *metricsConfig) {
if i == nil { if i == nil {
return return
} }
address := "" address := ""
if cfg != nil { if cfg != nil {
address = strings.TrimSpace(cfg.Address) address = strings.TrimSpace(cfg.Address)
} }
if address == "" { if address == "" {
i.logger.Info("Metrics endpoint disabled") i.logger.Info("Metrics endpoint disabled")
return return
} }
listener, err := net.Listen("tcp", address) lc := net.ListenConfig{}
listener, err := lc.Listen(context.Background(), "tcp", address)
if err != nil { if err != nil {
i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err)) i.logger.Error("Failed to bind metrics listener", zap.String("address", address), zap.Error(err))
return return
} }
@@ -38,7 +46,9 @@ func (i *Imp) startMetrics(cfg *metricsConfig) {
router.Handle("/metrics", promhttp.Handler()) router.Handle("/metrics", promhttp.Handler())
var healthRouter routers.Health var healthRouter routers.Health
if hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, ""); err != nil {
hr, err := routers.NewHealthRouter(i.logger.Named("metrics"), router, "")
if err != nil {
i.logger.Warn("Failed to initialise health router", zap.Error(err)) i.logger.Warn("Failed to initialise health router", zap.Error(err))
} else { } else {
hr.SetStatus(health.SSStarting) hr.SetStatus(health.SSStarting)
@@ -49,13 +59,16 @@ func (i *Imp) startMetrics(cfg *metricsConfig) {
i.metricsSrv = &http.Server{ i.metricsSrv = &http.Server{
Addr: address, Addr: address,
Handler: router, Handler: router,
ReadHeaderTimeout: 5 * time.Second, ReadHeaderTimeout: readHeaderTimeout,
} }
go func() { go func() {
i.logger.Info("Prometheus endpoint listening", zap.String("address", address)) i.logger.Info("Prometheus endpoint listening", zap.String("address", address))
if err := i.metricsSrv.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err)) serveErr := i.metricsSrv.Serve(listener)
if serveErr != nil && !errors.Is(serveErr, http.ErrServerClosed) {
i.logger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(serveErr))
if healthRouter != nil { if healthRouter != nil {
healthRouter.SetStatus(health.SSTerminating) healthRouter.SetStatus(health.SSTerminating)
} }
@@ -69,14 +82,18 @@ func (i *Imp) shutdownMetrics(ctx context.Context) {
i.metricsHealth.Finish() i.metricsHealth.Finish()
i.metricsHealth = nil i.metricsHealth = nil
} }
if i.metricsSrv == nil { if i.metricsSrv == nil {
return return
} }
if err := i.metricsSrv.Shutdown(ctx); err != nil && !errors.Is(err, http.ErrServerClosed) {
err := i.metricsSrv.Shutdown(ctx)
if err != nil && !errors.Is(err, http.ErrServerClosed) {
i.logger.Warn("Failed to stop metrics server", zap.Error(err)) i.logger.Warn("Failed to stop metrics server", zap.Error(err))
} else { } else {
i.logger.Info("Metrics server stopped") i.logger.Info("Metrics server stopped")
} }
i.metricsSrv = nil i.metricsSrv = nil
} }
@@ -84,5 +101,6 @@ func (i *Imp) setMetricsStatus(status health.ServiceStatus) {
if i == nil || i.metricsHealth == nil { if i == nil || i.metricsHealth == nil {
return return
} }
i.metricsHealth.SetStatus(status) i.metricsHealth.SetStatus(status)
} }

View File

@@ -10,6 +10,9 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
) )
const defaultShutdownTimeout = 15 * time.Second
// Create returns a new server implementation configured with the given logger, config file, and debug flag.
func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
return &Imp{ return &Imp{
logger: logger.Named("server"), logger: logger.Named("server"),
@@ -18,6 +21,7 @@ func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) {
}, nil }, nil
} }
// Start loads configuration, starts metrics and the discovery registry, then blocks until stopped.
func (i *Imp) Start() error { func (i *Imp) Start() error {
i.initStopChannels() i.initStopChannels()
defer i.closeDone() defer i.closeDone()
@@ -28,29 +32,37 @@ func (i *Imp) Start() error {
if err != nil { if err != nil {
return err return err
} }
i.config = cfg i.config = cfg
messagingDriver := "none" messagingDriver := "none"
if cfg.Messaging != nil { if cfg.Messaging != nil {
messagingDriver = string(cfg.Messaging.Driver) messagingDriver = string(cfg.Messaging.Driver)
} }
metricsAddress := "" metricsAddress := ""
if cfg.Metrics != nil { if cfg.Metrics != nil {
metricsAddress = strings.TrimSpace(cfg.Metrics.Address) metricsAddress = strings.TrimSpace(cfg.Metrics.Address)
} }
if metricsAddress == "" { if metricsAddress == "" {
metricsAddress = "disabled" metricsAddress = "disabled"
} }
i.logger.Info("Discovery config loaded", zap.String("messaging_driver", messagingDriver), zap.String("metrics_address", metricsAddress))
i.logger.Info("Discovery config loaded",
zap.String("messaging_driver", messagingDriver),
zap.String("metrics_address", metricsAddress))
i.startMetrics(cfg.Metrics) i.startMetrics(cfg.Metrics)
if err := i.startDiscovery(cfg); err != nil { err = i.startDiscovery(cfg)
if err != nil {
i.stopDiscovery() i.stopDiscovery()
i.setMetricsStatus(health.SSTerminating) i.setMetricsStatus(health.SSTerminating)
ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout()) ctx, cancel := context.WithTimeout(context.Background(), i.shutdownTimeout())
i.shutdownMetrics(ctx) i.shutdownMetrics(ctx)
cancel() cancel()
return err return err
} }
@@ -59,9 +71,11 @@ func (i *Imp) Start() error {
<-i.stopCh <-i.stopCh
i.logger.Info("Discovery service stop signal received") i.logger.Info("Discovery service stop signal received")
return nil return nil
} }
// Shutdown gracefully stops the discovery service and its metrics server.
func (i *Imp) Shutdown() { func (i *Imp) Shutdown() {
timeout := i.shutdownTimeout() timeout := i.shutdownTimeout()
i.logger.Info("Stopping discovery service", zap.Duration("timeout", timeout)) i.logger.Info("Stopping discovery service", zap.Duration("timeout", timeout))
@@ -72,6 +86,7 @@ func (i *Imp) Shutdown() {
if i.doneCh != nil { if i.doneCh != nil {
<-i.doneCh <-i.doneCh
} }
ctx, cancel := context.WithTimeout(context.Background(), timeout) ctx, cancel := context.WithTimeout(context.Background(), timeout)
i.shutdownMetrics(ctx) i.shutdownMetrics(ctx)
cancel() cancel()
@@ -83,6 +98,7 @@ func (i *Imp) initStopChannels() {
if i.stopCh == nil { if i.stopCh == nil {
i.stopCh = make(chan struct{}) i.stopCh = make(chan struct{})
} }
if i.doneCh == nil { if i.doneCh == nil {
i.doneCh = make(chan struct{}) i.doneCh = make(chan struct{})
} }
@@ -108,5 +124,6 @@ func (i *Imp) shutdownTimeout() time.Duration {
if i.config != nil && i.config.Runtime != nil { if i.config != nil && i.config.Runtime != nil {
return i.config.Runtime.ShutdownTimeout() return i.config.Runtime.ShutdownTimeout()
} }
return 15 * time.Second
return defaultShutdownTimeout
} }

View File

@@ -9,6 +9,7 @@ import (
"github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mlogger"
) )
// Imp is the concrete implementation of the discovery server application.
type Imp struct { type Imp struct {
logger mlogger.Logger logger mlogger.Logger
file string file string

View File

@@ -1,3 +1,4 @@
// Package server provides the discovery service application factory.
package server package server
import ( import (
@@ -6,6 +7,9 @@ import (
"github.com/tech/sendico/pkg/server" "github.com/tech/sendico/pkg/server"
) )
// Create initialises and returns a new discovery server application.
//
//nolint:ireturn // factory returns interface by design
func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
return serverimp.Create(logger, file, debug) return serverimp.Create(logger, file, debug) //nolint:wrapcheck
} }

View File

@@ -1,3 +1,4 @@
// Package main is the entry point for the discovery service.
package main package main
import ( import (
@@ -8,8 +9,9 @@ import (
smain "github.com/tech/sendico/pkg/server/main" smain "github.com/tech/sendico/pkg/server/main"
) )
//nolint:ireturn // factory returns interface by design
func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) {
return si.Create(logger, file, debug) return si.Create(logger, file, debug) //nolint:wrapcheck
} }
func main() { func main() {

View File

Before

Width:  |  Height:  |  Size: 2.2 KiB

After

Width:  |  Height:  |  Size: 2.2 KiB

View File

@@ -76,8 +76,8 @@ api:
root_path: ./storage root_path: ./storage
chain_gateway: chain_gateway:
address: dev-chain-gateway:50070 address: dev-tron-gateway:50071
address_env: CHAIN_GATEWAY_ADDRESS address_env: TRON_GATEWAY_ADDRESS
dial_timeout_seconds: 5 dial_timeout_seconds: 5
call_timeout_seconds: 5 call_timeout_seconds: 5
insecure: true insecure: true
@@ -97,6 +97,31 @@ api:
dial_timeout_seconds: 5 dial_timeout_seconds: 5
call_timeout_seconds: 5 call_timeout_seconds: 5
insecure: true insecure: true
payment_quotation:
address: dev-payments-quotation:50064
address_env: PAYMENTS_QUOTE_ADDRESS
dial_timeout_seconds: 5
call_timeout_seconds: 5
insecure: true
payment_methods:
address: dev-payments-methods:50066
address_env: PAYMENTS_METHODS_ADDRESS
dial_timeout_seconds: 5
call_timeout_seconds: 5
insecure: true
callbacks:
default_event_types:
- payment.status.updated
default_status: active
secret_path_prefix: sendico/callbacks
secret_field: value
secret_length_bytes: 32
vault:
address: "http://dev-vault:8200"
token_env: VAULT_TOKEN
token_file_env: VAULT_TOKEN_FILE
namespace: ""
mount_path: kv
app: app:

View File

@@ -16,7 +16,9 @@ api:
CORS: CORS:
max_age: 300 max_age: 300
allowed_origins: allowed_origins:
- "*" - "https://sendico.io"
- "https://app.sendico.io"
- "https://www.sendico.io"
allowed_methods: allowed_methods:
- "GET" - "GET"
- "POST" - "POST"
@@ -76,8 +78,8 @@ api:
root_path: ./storage root_path: ./storage
chain_gateway: chain_gateway:
address: sendico_chain_gateway:50070 address: sendico_tron_gateway:50071
address_env: CHAIN_GATEWAY_ADDRESS address_env: TRON_GATEWAY_ADDRESS
dial_timeout_seconds: 5 dial_timeout_seconds: 5
call_timeout_seconds: 5 call_timeout_seconds: 5
insecure: true insecure: true
@@ -97,6 +99,31 @@ api:
dial_timeout_seconds: 5 dial_timeout_seconds: 5
call_timeout_seconds: 5 call_timeout_seconds: 5
insecure: true insecure: true
payment_quotation:
address: sendico_payments_quotation:50064
address_env: PAYMENTS_QUOTE_ADDRESS
dial_timeout_seconds: 5
call_timeout_seconds: 5
insecure: true
payment_methods:
address: sendico_payments_methods:50066
address_env: PAYMENTS_METHODS_ADDRESS
dial_timeout_seconds: 5
call_timeout_seconds: 5
insecure: true
callbacks:
default_event_types:
- payment.status.updated
default_status: active
secret_path_prefix: sendico/callbacks
secret_field: value
secret_length_bytes: 32
vault:
address: "https://vault.sendico.io"
token_env: VAULT_TOKEN
token_file_env: VAULT_TOKEN_FILE
namespace: ""
mount_path: kv
app: app:

View File

@@ -1,38 +1,44 @@
module github.com/tech/sendico/server module github.com/tech/sendico/server
go 1.25.6 go 1.25.7
replace github.com/tech/sendico/pkg => ../pkg replace github.com/tech/sendico/pkg => ../../pkg
replace github.com/tech/sendico/ledger => ../ledger replace github.com/tech/sendico/ledger => ../../ledger
replace github.com/tech/sendico/payments/orchestrator => ../payments/orchestrator replace github.com/tech/sendico/payments/orchestrator => ../../payments/orchestrator
replace github.com/tech/sendico/gateway/chain => ../gateway/chain replace github.com/tech/sendico/payments/methods => ../../payments/methods
replace github.com/tech/sendico/payments/storage => ../../payments/storage
replace github.com/tech/sendico/gateway/tron => ../../gateway/tron
require ( require (
github.com/aws/aws-sdk-go-v2 v1.41.1 github.com/aws/aws-sdk-go-v2 v1.41.3
github.com/aws/aws-sdk-go-v2/config v1.32.7 github.com/aws/aws-sdk-go-v2/config v1.32.11
github.com/aws/aws-sdk-go-v2/credentials v1.19.7 github.com/aws/aws-sdk-go-v2/credentials v1.19.11
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0 github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3
github.com/go-chi/chi/v5 v5.2.4 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.3.3 github.com/go-chi/jwtauth/v5 v5.4.0
github.com/go-chi/metrics v0.1.1 github.com/go-chi/metrics v0.1.1
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/mitchellh/mapstructure v1.5.0 github.com/mitchellh/mapstructure v1.5.0
github.com/prometheus/client_golang v1.23.2
github.com/shopspring/decimal v1.4.0 github.com/shopspring/decimal v1.4.0
github.com/stretchr/testify v1.11.1 github.com/stretchr/testify v1.11.1
github.com/tech/sendico/gateway/chain v0.0.0-00010101000000-000000000000 github.com/tech/sendico/gateway/tron v0.0.0-00010101000000-000000000000
github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000 github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000
github.com/tech/sendico/payments/methods v0.0.0-00010101000000-000000000000
github.com/tech/sendico/payments/orchestrator v0.0.0-00010101000000-000000000000 github.com/tech/sendico/payments/orchestrator v0.0.0-00010101000000-000000000000
github.com/tech/sendico/pkg v0.1.0 github.com/tech/sendico/pkg v0.1.0
github.com/testcontainers/testcontainers-go v0.33.0 github.com/testcontainers/testcontainers-go v0.33.0
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.49.0 golang.org/x/net v0.51.0
google.golang.org/grpc v1.78.0 google.golang.org/grpc v1.79.1
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
@@ -48,21 +54,21 @@ 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.4 // 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.17 // 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.17 // 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.17 // 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.17 // indirect github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 // indirect
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 // 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.8 // 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.17 // 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.17 // 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.5 // 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.9 // 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.13 // 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.6 // indirect github.com/aws/aws-sdk-go-v2/service/sts v1.41.8 // indirect
github.com/aws/smithy-go v1.24.0 // 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
github.com/cenkalti/backoff/v4 v4.3.0 // indirect github.com/cenkalti/backoff/v4 v4.3.0 // indirect
@@ -71,30 +77,42 @@ require (
github.com/containerd/platforms v0.2.1 // indirect github.com/containerd/platforms v0.2.1 // indirect
github.com/cpuguy83/dockercfg v0.3.2 // indirect github.com/cpuguy83/dockercfg v0.3.2 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 // indirect github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 // indirect
github.com/distribution/reference v0.6.0 // indirect github.com/distribution/reference v0.6.0 // indirect
github.com/docker/docker v27.3.1+incompatible // indirect github.com/docker/docker v27.3.1+incompatible // indirect
github.com/docker/go-connections v0.5.0 // indirect github.com/docker/go-connections v0.5.0 // indirect
github.com/docker/go-units v0.5.0 // indirect github.com/docker/go-units v0.5.0 // indirect
github.com/felixge/httpsnoop v1.0.4 // indirect github.com/felixge/httpsnoop v1.0.4 // indirect
github.com/go-chi/chi v1.5.5 // indirect github.com/go-chi/chi v1.5.5 // indirect
github.com/go-jose/go-jose/v4 v4.1.3 // indirect
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.5 // indirect
github.com/gogo/protobuf v1.3.2 // indirect github.com/gogo/protobuf v1.3.2 // indirect
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 // indirect github.com/hashicorp/errwrap v1.1.0 // indirect
github.com/klauspost/compress v1.18.3 // indirect github.com/hashicorp/go-cleanhttp v0.5.2 // indirect
github.com/hashicorp/go-multierror v1.1.1 // indirect
github.com/hashicorp/go-retryablehttp v0.7.8 // indirect
github.com/hashicorp/go-rootcerts v1.0.2 // indirect
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 // indirect
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 // indirect
github.com/hashicorp/go-sockaddr v1.0.7 // indirect
github.com/hashicorp/hcl v1.0.1-vault-7 // indirect
github.com/hashicorp/vault/api v1.22.0 // indirect
github.com/klauspost/compress v1.18.4 // indirect
github.com/lestrrat-go/blackmagic v1.0.4 // indirect github.com/lestrrat-go/blackmagic v1.0.4 // indirect
github.com/lestrrat-go/dsig v1.0.0 // indirect
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 // indirect
github.com/lestrrat-go/httpcc v1.0.1 // indirect github.com/lestrrat-go/httpcc v1.0.1 // indirect
github.com/lestrrat-go/httprc v1.0.6 // indirect github.com/lestrrat-go/httprc/v3 v3.0.4 // indirect
github.com/lestrrat-go/iter v1.0.2 // indirect github.com/lestrrat-go/jwx/v3 v3.0.13 // indirect
github.com/lestrrat-go/jwx/v2 v2.1.6 // indirect github.com/lestrrat-go/option/v2 v2.0.0 // indirect
github.com/lestrrat-go/option v1.0.1 // indirect
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
github.com/magiconair/properties v1.8.7 // indirect github.com/magiconair/properties v1.8.7 // indirect
github.com/mattn/go-colorable v0.1.14 // indirect github.com/mattn/go-colorable v0.1.14 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mitchellh/go-homedir v1.1.0 // indirect
github.com/moby/docker-image-spec v1.3.1 // indirect github.com/moby/docker-image-spec v1.3.1 // indirect
github.com/moby/patternmatcher v0.6.0 // indirect github.com/moby/patternmatcher v0.6.0 // indirect
github.com/moby/sys/sequential v0.6.0 // indirect github.com/moby/sys/sequential v0.6.0 // indirect
@@ -103,7 +121,7 @@ require (
github.com/moby/term v0.5.0 // indirect github.com/moby/term v0.5.0 // indirect
github.com/morikuni/aec v1.0.0 // indirect github.com/morikuni/aec v1.0.0 // indirect
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect
github.com/nats-io/nats.go v1.48.0 // indirect github.com/nats-io/nats.go v1.49.0 // indirect
github.com/nats-io/nkeys v0.4.15 // indirect github.com/nats-io/nkeys v0.4.15 // indirect
github.com/nats-io/nuid v1.0.1 // indirect github.com/nats-io/nuid v1.0.1 // indirect
github.com/opencontainers/go-digest v1.0.0 // indirect github.com/opencontainers/go-digest v1.0.0 // indirect
@@ -111,16 +129,17 @@ require (
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/prometheus/client_golang v1.23.2 // indirect
github.com/prometheus/client_model v0.6.2 // indirect github.com/prometheus/client_model v0.6.2 // indirect
github.com/prometheus/common v0.67.5 // indirect github.com/prometheus/common v0.67.5 // indirect
github.com/prometheus/procfs v0.19.2 // indirect github.com/prometheus/procfs v0.20.1 // indirect
github.com/ryanuber/go-glob v1.0.0 // indirect
github.com/segmentio/asm v1.2.1 // indirect github.com/segmentio/asm v1.2.1 // indirect
github.com/shirou/gopsutil/v3 v3.24.5 // indirect github.com/shirou/gopsutil/v3 v3.24.5 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/sirupsen/logrus v1.9.3 // indirect github.com/sirupsen/logrus v1.9.3 // indirect
github.com/tklauser/go-sysconf v0.3.16 // indirect github.com/tklauser/go-sysconf v0.3.16 // indirect
github.com/tklauser/numcpus v0.11.0 // indirect github.com/tklauser/numcpus v0.11.0 // indirect
github.com/valyala/fastjson v1.6.10 // indirect
github.com/xdg-go/pbkdf2 v1.0.0 // indirect github.com/xdg-go/pbkdf2 v1.0.0 // indirect
github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/scram v1.2.0 // indirect
github.com/xdg-go/stringprep v1.0.4 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect
@@ -128,15 +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.39.0 // indirect go.opentelemetry.io/otel v1.41.0 // indirect
go.opentelemetry.io/otel/metric v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 // indirect
go.opentelemetry.io/otel/sdk v1.39.0 // indirect go.opentelemetry.io/otel/metric v1.41.0 // indirect
go.opentelemetry.io/otel/trace v1.39.0 // indirect go.opentelemetry.io/otel/sdk/metric v1.40.0 // indirect
go.opentelemetry.io/otel/trace v1.41.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.3 // indirect
golang.org/x/crypto v0.47.0 // indirect golang.org/x/crypto v0.48.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect
golang.org/x/sys v0.40.0 // indirect golang.org/x/sys v0.41.0 // indirect
golang.org/x/text v0.33.0 // indirect golang.org/x/text v0.34.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b // indirect golang.org/x/time v0.14.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 // indirect
) )

View File

@@ -6,44 +6,44 @@ 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.1 h1:ABlyEARCDLN034NhxlRUSZr4l71mh+T5KAeGh6cerhU= github.com/aws/aws-sdk-go-v2 v1.41.3 h1:4kQ/fa22KjDt13QCy1+bYADvdgcxpfH18f0zP542kZA=
github.com/aws/aws-sdk-go-v2 v1.41.1/go.mod h1:MayyLB8y+buD9hZqkCW3kX1AKq07Y5pXxtgB+rRFhz0= 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.4 h1:489krEF9xIGkOaaX3CE/Be2uWjiXrkCH6gUX+bZA/BU= 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.4/go.mod h1:IOAPF6oT9KCsceNTvvYMNHy0+kMF8akOjeDvPENWxp4= 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.7 h1:vxUyWGUwmkQ2g19n7JY/9YL8MfAIl7bTesIUykECXmY= github.com/aws/aws-sdk-go-v2/config v1.32.11 h1:ftxI5sgz8jZkckuUHXfC/wMUc8u3fG1vQS0plr2F2Zs=
github.com/aws/aws-sdk-go-v2/config v1.32.7/go.mod h1:2/Qm5vKUU/r7Y+zUk/Ptt2MDAEKAfUtKc1+3U1Mo3oY= 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.7 h1:tHK47VqqtJxOymRrNtUXN5SP/zUTvZKeLx4tH6PGQc8= github.com/aws/aws-sdk-go-v2/credentials v1.19.11 h1:NdV8cwCcAXrCWyxArt58BrvZJ9pZ9Fhf9w6Uh5W3Uyc=
github.com/aws/aws-sdk-go-v2/credentials v1.19.7/go.mod h1:qOZk8sPDrxhf+4Wf4oT2urYJrYt3RejHSzgAquYeppw= 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.17 h1:I0GyV8wiYrP8XpA70g1HBcQO1JlQxCMTW9npl5UbDHY= 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.17/go.mod h1:tyw7BOl5bBe/oqvoIeECFJjMdzXoa/dfVz3QQ5lgHGA= 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.17 h1:xOLELNKGp2vsiteLsvLPwxC+mYmO6OZ8PYgiuPJzF8U= 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.17/go.mod h1:5M5CI3D12dNOtH3/mk6minaRwI2/37ifCURZISxA/IQ= 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.17 h1:WWLqlh79iO48yLkj1v3ISRNiv+3KdQoZ6JWyfcsyQik= 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.17/go.mod h1:EhG22vHRrvF8oXSTYStZhJc1aUgKtnJe+aOiFEV90cM= 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.17 h1:JqcdRG//czea7Ppjb+g/n4o8i/R50aTBHkA7vu0lK+k= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19 h1:3Y4oma5TiV7tT9wa8zRcdoXwZkGz9Q/wxbEUK7cMuAM=
github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.17/go.mod h1:CO+WeGmIdj/MlPel2KwID9Gt7CNq4M65HUfBW97liM0= github.com/aws/aws-sdk-go-v2/internal/v4a v1.4.19/go.mod h1:V1K+TeJVD5JOk3D9e5tsX2KUdL7BlB+FV6cBhdobN8c=
github.com/aws/aws-sdk-go-v2/service/internal/accept-encoding v1.13.4 h1:0ryTNEdJbzUCEWkVXEXoqlXV72J5keC1GvILMOuD00E= 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.4/go.mod h1:HQ4qwNZh32C3CBeO6iJLQlgtMzqeG17ziAA/3KDJFow= 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.8 h1:Z5EiPIzXKewUQK0QTMkutjiaPVeVYXX7KIqhXu/0fXs= 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.8/go.mod h1:FsTpJtvC4U1fyDXk7c71XoDv3HlRm8V3NiYLeYLh5YE= 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.17 h1:RuNSMoozM8oXlgLG/n6WLaFGoea7/CddrCfIiSA+xdY= 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.17/go.mod h1:F2xxQ9TZz5gDWsclCtPQscGpP0VUOc8RqgFM3vDENmU= 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.17 h1:bGeHBsGZx0Dvu/eJC0Lh9adJa3M1xREcndxLNZlve2U= 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.17/go.mod h1:dcW24lbU0CzHusTE8LLHhRLI42ejmINN8Lcr22bwh/g= 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.0 h1:oeu8VPlOre74lBA/PMhxa5vewaMIMmILM+RraSyB8KA= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3 h1:+d0SsTvxtIJt4tSJ6wr+jrxEMDa6XeupjRv8H7Qitkk=
github.com/aws/aws-sdk-go-v2/service/s3 v1.96.0/go.mod h1:5jggDlZ2CLQhwJBiZJb4vfk4f0GxWdEDruWKEJ1xOdo= github.com/aws/aws-sdk-go-v2/service/s3 v1.96.3/go.mod h1:ROUNFvFWPwBlOu687WJNQ9cPvd2ccpFrnCiA1YGz50o=
github.com/aws/aws-sdk-go-v2/service/signin v1.0.5 h1:VrhDvQib/i0lxvr3zqlUwLwJP4fpmpyD9wYG1vfSu+Y= 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.5/go.mod h1:k029+U8SY30/3/ras4G/Fnv/b88N4mAfliNn08Dem4M= 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.9 h1:v6EiMvhEYBoHABfbGB4alOYmCIrcgyPPiBE1wZAEbqk= 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.9/go.mod h1:yifAsgBxgJWn3ggx70A3urX2AN49Y5sJTD1UQFlfqBw= 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.13 h1:gd84Omyu9JLriJVCbGApcLzVR3XtmC4ZDPcAI6Ftvds= 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.13/go.mod h1:sTGThjphYE4Ohw8vJiRStAcu3rbjtXRsdNB0TvZ5wwo= 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.6 h1:5fFjR/ToSOzB2OQ/XqWpZBmNvmP/pJ1jOWYlFDJTjRQ= 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.6/go.mod h1:qgFDZQSD/Kys7nJnVqYlWKnh0SSdMjAi0uSwON4wgYQ= 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.0 h1:LpilSUItNPFr1eY85RYgTIg5eIEPtvFbskaFcmmIUnk= github.com/aws/smithy-go v1.24.2 h1:FzA3bu/nt/vDvmnkg+R8Xl46gmzEDam6mZ1hzmwXFng=
github.com/aws/smithy-go v1.24.0/go.mod h1:LEj2LM3rBRQJxPZTB4KuzZkaZYnZPnvgIhb4pu07mx0= 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=
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM= github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw= github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
@@ -59,6 +59,8 @@ github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0l
github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE= github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE=
github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8=
github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE= github.com/cenkalti/backoff/v4 v4.3.0/go.mod h1:Y3VNntkOUPxTVeUxJ/G5vcM//AlwfmyYozVcomhLiZE=
github.com/cenkalti/backoff/v5 v5.0.3 h1:ZN+IMa753KfX5hd8vVaMixjnqRZ3y8CuJKRKj1xcsSM=
github.com/cenkalti/backoff/v5 v5.0.3/go.mod h1:rkhZdG3JZukswDf7f0cwqPNk4K0sa+F97BxZthm/crw=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I=
@@ -73,8 +75,8 @@ github.com/creack/pty v1.1.18/go.mod h1:MOBLtS5ELjhRRrroQr9kyvTxUAFNvYEK993ew/Vr
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38= github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1 h1:5RVFMOWjMyRy8cARdy79nAmgYw3hK/4HUq48LQ6Wwqo=
github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.1/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40=
github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk= github.com/distribution/reference v0.6.0 h1:0IXCQ5g4/QMHHkarYzh5l+u8T3t73zM5QvfrDyIgxBk=
github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E= github.com/distribution/reference v0.6.0/go.mod h1:BbU0aIcezP1/5jX/8MP0YiH4SdvB5Y4f/wlDRiLyi3E=
github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= github.com/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI=
@@ -83,19 +85,23 @@ github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj
github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc= github.com/docker/go-connections v0.5.0/go.mod h1:ov60Kzw0kKElRwhNs9UlUHAE/F9Fe6GLaXnqyDdmEXc=
github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4= github.com/docker/go-units v0.5.0 h1:69rxXcBk27SvSaaxTtLh/8llcHD8vYHT7WSdRZ/jvr4=
github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= github.com/docker/go-units v0.5.0/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk=
github.com/fatih/color v1.18.0 h1:S8gINlzdQ840/4pfAwic/ZE0djQEH3wM94VfqLTZcOM=
github.com/fatih/color v1.18.0/go.mod h1:4FelSpRwEGDpQ12mAdzqdOukCy4u8WUtOY6lkT/6HfU=
github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg=
github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U=
github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg= github.com/go-chi/chi v1.5.4/go.mod h1:uaf8YgoFazUOkPBG7fxPftUylNumIev9awIWOENIuEg=
github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE= github.com/go-chi/chi v1.5.5 h1:vOB/HbEMt9QqBqErz07QehcOKHaWFtuj87tTDVz2qXE=
github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw= github.com/go-chi/chi v1.5.5/go.mod h1:C9JqLr3tIYjDOZpzn+BCuxY8z8vmca43EeMgyZt7irw=
github.com/go-chi/chi/v5 v5.2.4 h1:WtFKPHwlywe8Srng8j2BhOD9312j9cGUxG1SP4V2cR4= github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
github.com/go-chi/chi/v5 v5.2.4/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE= github.com/go-chi/cors v1.2.2 h1:Jmey33TE+b+rB7fT8MUy1u0I4L+NARQlK6LhzKPSyQE=
github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58= github.com/go-chi/cors v1.2.2/go.mod h1:sSbTewc+6wYHBBCW7ytsFSn836hqM7JxpglAy2Vzc58=
github.com/go-chi/jwtauth/v5 v5.3.3 h1:50Uzmacu35/ZP9ER2Ht6SazwPsnLQ9LRJy6zTZJpHEo= github.com/go-chi/jwtauth/v5 v5.4.0 h1:Ieh0xMJsFvqylqJ02/mQHKzbbKO9DYNBh4DPKCwTwYI=
github.com/go-chi/jwtauth/v5 v5.3.3/go.mod h1:O4QvPRuZLZghl9WvfVaON+ARfGzpD2PBX/QY5vUz7aQ= github.com/go-chi/jwtauth/v5 v5.4.0/go.mod h1:w6yjqUUXz1b8+oiJel64Sz1KJwduQM6qUA5QNzO5+bQ=
github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk= github.com/go-chi/metrics v0.1.1 h1:CXhbnkAVVjb0k73EBRQ6Z2YdWFnbXZgNtg1Mboguibk=
github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4= github.com/go-chi/metrics v0.1.1/go.mod h1:mcGTM1pPalP7WCtb+akNYFO/lwNwBBLCuedepqjoPn4=
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A= github.com/go-logr/logr v1.2.2/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
@@ -104,6 +110,8 @@ github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE= 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/go.mod h1:5C2ZWiW0ErCdrYzpqxLbTX7MG14M9iiw8DgHncVwcsE=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/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=
@@ -119,12 +127,35 @@ github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU= github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2 h1:8Tjv8EJ+pM1xP8mK6egEbD1OgnVTyacbefKhmbLhIhU= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7 h1:X+2YciYSxvMQK0UZ7sg45ZVabVZBeBuvMkmuI2V3Fak=
github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.2/go.mod h1:pkJQ2tZHJ0aFOVEEot6oZmaVEZcRme73eIFmhiVuRWs= github.com/grpc-ecosystem/grpc-gateway/v2 v2.27.7/go.mod h1:lW34nIZuQ8UDPdkon5fmfp2l3+ZkQ2me/+oecHYLOII=
github.com/hashicorp/errwrap v1.0.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/errwrap v1.1.0 h1:OxrOeh75EUXMY8TBjag2fzXGZ40LB6IKw45YeGUDY2I=
github.com/hashicorp/errwrap v1.1.0/go.mod h1:YH+1FKiLXxHSkmPseP+kNlulaMuP3n2brvKWEqk/Jc4=
github.com/hashicorp/go-cleanhttp v0.5.2 h1:035FKYIWjmULyFRBKPs8TBQoi0x6d9G4xc9neXJWAZQ=
github.com/hashicorp/go-cleanhttp v0.5.2/go.mod h1:kO/YDlP8L1346E6Sodw+PrpBSV4/SoxCXGY6BqNFT48=
github.com/hashicorp/go-hclog v1.6.3 h1:Qr2kF+eVWjTiYmU7Y31tYlP1h0q/X3Nl3tPGdaB11/k=
github.com/hashicorp/go-hclog v1.6.3/go.mod h1:W4Qnvbt70Wk/zYJryRzDRU/4r0kIg0PVHBcfoyhpF5M=
github.com/hashicorp/go-multierror v1.1.1 h1:H5DkEtf6CXdFp0N0Em5UCwQpXMWke8IA0+lD48awMYo=
github.com/hashicorp/go-multierror v1.1.1/go.mod h1:iw975J/qwKPdAO1clOe2L8331t/9/fmwbPZ6JB6eMoM=
github.com/hashicorp/go-retryablehttp v0.7.8 h1:ylXZWnqa7Lhqpk0L1P1LzDtGcCR0rPVUrx/c8Unxc48=
github.com/hashicorp/go-retryablehttp v0.7.8/go.mod h1:rjiScheydd+CxvumBsIrFKlx3iS0jrZ7LvzFGFmuKbw=
github.com/hashicorp/go-rootcerts v1.0.2 h1:jzhAVGtqPKbwpyCPELlgNWhE1znq+qwJtW5Oi2viEzc=
github.com/hashicorp/go-rootcerts v1.0.2/go.mod h1:pqUvnprVnM5bf7AOirdbb01K4ccR319Vf4pU3K5EGc8=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0 h1:U+kC2dOhMFQctRfhK0gRctKAPTloZdMU5ZJxaesJ/VM=
github.com/hashicorp/go-secure-stdlib/parseutil v0.2.0/go.mod h1:Ll013mhdmsVDuoIXVfBtvgGJsXDYkTw1kooNcoCXuE0=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2 h1:kes8mmyCpxJsI7FTwtzRqEy9CdjCtrXrXGuOpxEA7Ts=
github.com/hashicorp/go-secure-stdlib/strutil v0.1.2/go.mod h1:Gou2R9+il93BqX25LAKCLuM+y9U2T4hlwvT1yprcna4=
github.com/hashicorp/go-sockaddr v1.0.7 h1:G+pTkSO01HpR5qCxg7lxfsFEZaG+C0VssTy/9dbT+Fw=
github.com/hashicorp/go-sockaddr v1.0.7/go.mod h1:FZQbEYa1pxkQ7WLpyXJ6cbjpT8q0YgQaK/JakXqGyWw=
github.com/hashicorp/hcl v1.0.1-vault-7 h1:ag5OxFVy3QYTFTJODRzTKVZ6xvdfLLCA1cy/Y6xGI0I=
github.com/hashicorp/hcl v1.0.1-vault-7/go.mod h1:XYhtn6ijBSAj6n4YqAaf7RBPS4I06AItNorpy+MoQNM=
github.com/hashicorp/vault/api v1.22.0 h1:+HYFquE35/B74fHoIeXlZIP2YADVboaPjaSicHEZiH0=
github.com/hashicorp/vault/api v1.22.0/go.mod h1:IUZA2cDvr4Ok3+NtK2Oq/r+lJeXkeCrHRmqdyWfpmGM=
github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8= github.com/kisielk/errcheck v1.5.0/go.mod h1:pFxgyoBC7bSaBwPgfKdkLd5X25qrDl4LWUI2bnpBCr8=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.18.3 h1:9PJRvfbmTabkOX8moIpXPbMMbYN60bWImDDU7L+/6zw= github.com/klauspost/compress v1.18.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c=
github.com/klauspost/compress v1.18.3/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo= github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI= github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
@@ -137,16 +168,18 @@ github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA= github.com/lestrrat-go/blackmagic v1.0.4 h1:IwQibdnf8l2KoO+qC3uT4OaTWsW7tuRQXy9TRN9QanA=
github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw= github.com/lestrrat-go/blackmagic v1.0.4/go.mod h1:6AWFyKNNj0zEXQYfTMPfZrAXUWUfTIZ5ECEUEJaijtw=
github.com/lestrrat-go/dsig v1.0.0 h1:OE09s2r9Z81kxzJYRn07TFM9XA4akrUdoMwr0L8xj38=
github.com/lestrrat-go/dsig v1.0.0/go.mod h1:dEgoOYYEJvW6XGbLasr8TFcAxoWrKlbQvmJgCR0qkDo=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0 h1:JpDe4Aybfl0soBvoVwjqDbp+9S1Y2OM7gcrVVMFPOzY=
github.com/lestrrat-go/dsig-secp256k1 v1.0.0/go.mod h1:CxUgAhssb8FToqbL8NjSPoGQlnO4w3LG1P0qPWQm/NU=
github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE= github.com/lestrrat-go/httpcc v1.0.1 h1:ydWCStUeJLkpYyjLDHihupbn2tYmZ7m22BGkcvZZrIE=
github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E= github.com/lestrrat-go/httpcc v1.0.1/go.mod h1:qiltp3Mt56+55GPVCbTdM9MlqhvzyuL6W/NMDA8vA5E=
github.com/lestrrat-go/httprc v1.0.6 h1:qgmgIRhpvBqexMJjA/PmwSvhNk679oqD1RbovdCGW8k= github.com/lestrrat-go/httprc/v3 v3.0.4 h1:pXyH2ppK8GYYggygxJ3TvxpCZnbEUWc9qSwRTTApaLA=
github.com/lestrrat-go/httprc v1.0.6/go.mod h1:mwwz3JMTPBjHUkkDv/IGJ39aALInZLrhBp0X7KGUZlo= github.com/lestrrat-go/httprc/v3 v3.0.4/go.mod h1:mSMtkZW92Z98M5YoNNztbRGxbXHql7tSitCvaxvo9l0=
github.com/lestrrat-go/iter v1.0.2 h1:gMXo1q4c2pHmC3dn8LzRhJfP1ceCbgSiT9lUydIzltI= github.com/lestrrat-go/jwx/v3 v3.0.13 h1:AdHKiPIYeCSnOJtvdpipPg/0SuFh9rdkN+HF3O0VdSk=
github.com/lestrrat-go/iter v1.0.2/go.mod h1:Momfcq3AnRlRjI5b5O8/G5/BvpzrhoFTZcn06fEOPt4= github.com/lestrrat-go/jwx/v3 v3.0.13/go.mod h1:2m0PV1A9tM4b/jVLMx8rh6rBl7F6WGb3EG2hufN9OQU=
github.com/lestrrat-go/jwx/v2 v2.1.6 h1:hxM1gfDILk/l5ylers6BX/Eq1m/pnxe9NBwW6lVfecA= github.com/lestrrat-go/option/v2 v2.0.0 h1:XxrcaJESE1fokHy3FpaQ/cXW8ZsIdWcdFzzLOcID3Ss=
github.com/lestrrat-go/jwx/v2 v2.1.6/go.mod h1:Y722kU5r/8mV7fYDifjug0r8FK8mZdw0K0GpJw/l8pU= github.com/lestrrat-go/option/v2 v2.0.0/go.mod h1:oSySsmzMoR0iRzCDCaUfsCzxQHUEuhOViQObyy7S6Vg=
github.com/lestrrat-go/option v1.0.1 h1:oAzP2fvZGQKWkvHa1/SAcFolBEca1oN+mQ7eooNBEYU=
github.com/lestrrat-go/option v1.0.1/go.mod h1:5ZHFbivi4xwXxhxY9XHDe2FHo6/Z7WWmtT7T5nBBp3I=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg= github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
@@ -155,6 +188,8 @@ github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHP
github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8= github.com/mattn/go-colorable v0.1.14/go.mod h1:6LmQG8QLFO4G5z1gPvYEzlUgJ2wF+stgPZH1UqBm1s8=
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY= github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY= github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0=
@@ -169,14 +204,14 @@ github.com/moby/sys/userns v0.1.0 h1:tVLXkFOxVu9A64/yh59slHVv9ahO9UIev4JZusOLG/g
github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28= github.com/moby/sys/userns v0.1.0/go.mod h1:IHUYgu/kao6N8YZlp9Cf444ySSvCmDlmzUcYfDHOl28=
github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= github.com/moby/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0=
github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y= github.com/moby/term v0.5.0/go.mod h1:8FzsFHVUBGZdbDsJw/ot+X+d5HLUbvklYLJ9uGfcI3Y=
github.com/montanaflynn/stats v0.7.1 h1:etflOAAHORrCC44V+aR6Ftzort912ZU+YLiSTuV8eaE= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe h1:iruDEfMl2E6fbMZ9s0scYfZQ84/6SPL6zC8ACM2oIL0=
github.com/montanaflynn/stats v0.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= github.com/montanaflynn/stats v0.0.0-20171201202039-1bf9dbcd8cbe/go.mod h1:wL8QJuTMNUDYhXwkmfOly8iTdp5TEcJFWZD2D7SIkUc=
github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A=
github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 h1:C3w9PqII01/Oq1c1nUAm88MOHcQC9l5mIlSMApZMrHA=
github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ=
github.com/nats-io/nats.go v1.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= github.com/nats-io/nats.go v1.49.0 h1:yh/WvY59gXqYpgl33ZI+XoVPKyut/IcEaqtsiuTJpoE=
github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= github.com/nats-io/nats.go v1.49.0/go.mod h1:fDCn3mN5cY8HooHwE2ukiLb4p4G4ImmzvXyJt+tGwdw=
github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4=
github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs=
github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw=
@@ -200,10 +235,12 @@ github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNw
github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE=
github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= github.com/prometheus/common v0.67.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4=
github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw=
github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= github.com/prometheus/procfs v0.20.1 h1:XwbrGOIplXW/AU3YhIhLODXMJYyC1isLFfYCsTEycfc=
github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= github.com/prometheus/procfs v0.20.1/go.mod h1:o9EMBZGRyvDrSPH1RqdxhojkuXstoe4UlK79eF5TGGo=
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ= github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/ryanuber/go-glob v1.0.0 h1:iQh3xXAumdQ+4Ufa5b25cRpC5TYKlno6hsv6Cb3pkBk=
github.com/ryanuber/go-glob v1.0.0/go.mod h1:807d1WSdnB0XRJzKNil9Om6lcp/3a0v4qIHxIXzX/Yc=
github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0= github.com/segmentio/asm v1.2.1 h1:DTNbBqs57ioxAD4PrArqftgypG4/qNpXoJx8TVXxPR0=
github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs= github.com/segmentio/asm v1.2.1/go.mod h1:BqMnlJP91P8d+4ibuonYZw9mfnzI9HfxselHZr5aAcs=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
@@ -222,7 +259,6 @@ github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4= github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
@@ -236,6 +272,8 @@ github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYI
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI= github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw= github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ= github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
github.com/valyala/fastjson v1.6.10 h1:/yjJg8jaVQdYR3arGxPE2X5z89xrlhS0eGXdv+ADTh4=
github.com/valyala/fastjson v1.6.10/go.mod h1:e6FubmQouUNP73jtMLmcbxS6ydWIpOfhz34TSfO3JaE=
github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c= github.com/xdg-go/pbkdf2 v1.0.0 h1:Su7DPu48wXMwC3bs7MCNG+z4FhcyEuz5dlvchbq0B0c=
github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI= github.com/xdg-go/pbkdf2 v1.0.0/go.mod h1:jrpuAogTd400dnrH08LKmI/xc1MbPOebTwRqcT5RDeI=
github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= github.com/xdg-go/scram v1.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs=
@@ -258,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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= go.opentelemetry.io/otel v1.41.0 h1:YlEwVsGAlCvczDILpUXpIpPSL/VPugt7zHThEMLce1c=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel v1.41.0/go.mod h1:Yt4UwgEKeT05QbLwbyHXEwhnjxNO6D8L5PQP51/46dE=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0 h1:f0cb2XPmrqn4XMy9PNliTgRKJgS5WcL/u0/WRYGz4t0= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.40.0 h1:QKdN8ly8zEMrByybbQgv8cWBcdAarwmIPZ6FThrWXJs=
go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.39.0/go.mod h1:vnakAaFckOMiMtOIhFI2MNH4FYrZzXCYxmb1LlhoGz8= 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.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0 h1:wVZXIWjQSeSmMoxF74LzAnpVQOAFDo3pPji9Y4SOFKc=
go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.40.0/go.mod h1:khvBS2IggMFNwZK/6lEeHg/W57h/IX6J4URh57fuI40=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= go.opentelemetry.io/otel/metric v1.41.0 h1:rFnDcs4gRzBcsO9tS8LCpgR0dxg4aaxWlJxCno7JlTQ=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= go.opentelemetry.io/otel/metric v1.41.0/go.mod h1:xPvCwd9pU0VN8tPZYzDZV/BMj9CM9vs00GuBjeKhJps=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= go.opentelemetry.io/otel/sdk v1.40.0 h1:KHW/jUzgo6wsPh9At46+h4upjtccTmuZCFAc9OJ71f8=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= go.opentelemetry.io/otel/sdk v1.40.0/go.mod h1:Ph7EFdYvxq72Y8Li9q8KebuYUr2KoeyHx0DRMKrYBUE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= go.opentelemetry.io/otel/sdk/metric v1.40.0 h1:mtmdVqgQkeRxHgRv4qhyJduP3fYJRMX4AtAlbuWdCYw=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= go.opentelemetry.io/otel/sdk/metric v1.40.0/go.mod h1:4Z2bGMf0KSK3uRjlczMOeMhKU2rhUqdWNoKcYrtcBPg=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= go.opentelemetry.io/otel/trace v1.41.0 h1:Vbk2co6bhj8L59ZJ6/xFTskY+tGAbOnCtQGVVa9TIN0=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/otel/trace v1.41.0/go.mod h1:U1NU4ULCoxeDKc09yCWdWe+3QoyweJcISEVa1RBzOis=
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=
@@ -292,8 +330,8 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
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.47.0 h1:V6e3FRj+n4dbpw86FJ8Fv7XVOql7TEwpHapKoMJ/GO8= golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
golang.org/x/crypto v0.47.0/go.mod h1:ff3Y9VzzKbwSSEzWqJsJVBnWmRwRSHt/6Op5n9bQc4A= golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
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=
@@ -308,8 +346,8 @@ 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.49.0 h1:eeHFmOGUTtaaPSGNmjBKpbng9MulQsJURQUAfUwY++o= golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
golang.org/x/net v0.49.0/go.mod h1:/ysNB2EvaqvesRkuLAyjI1ycPZlQHM3q01F02UY/MV8= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
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=
@@ -332,18 +370,18 @@ 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.40.0 h1:DBZZqJ2Rkml6QMQsZywtnjnnGvHza6BTfYFWY9kjEWQ= golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
golang.org/x/sys v0.40.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
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.39.0 h1:RclSuaJf32jOqZz74CkPA9qFuVTX7vhLlpfj/IGWlqY= golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
golang.org/x/term v0.39.0/go.mod h1:yxzUCTP/U+FzoxfdKmLaA0RV1WgE0VY7hXBwKtY/4ww= golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
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.33.0 h1:B3njUFyqtHDUI5jMn1YIr5B0IE2U0qck04r6d4KPAxE= golang.org/x/text v0.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk=
golang.org/x/text v0.33.0/go.mod h1:LuMebE6+rBincTi9+xWTY8TztLzKHc/9C1uBCG27+q8= golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA=
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI= golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4= golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
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=
@@ -361,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-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171 h1:tu/dtnW1o3wfaxCOjSLn5IRX4YDcJrtlpzYkhHhGaC4=
google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= google.golang.org/genproto/googleapis/api v0.0.0-20260226221140-a57be14db171/go.mod h1:M5krXqk4GhBKvB596udGL3UyjL4I1+cTbK0orROM9ng=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b h1:GZxXGdFaHX27ZSMHudWc4FokdD+xl8BC2UJm1OVIEzs= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171 h1:ggcbiqK8WWh6l1dnltU4BgWGIGo+EVYxCaAPih/zQXQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260202165425-ce8ad4cf556b/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20260226221140-a57be14db171/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY=
google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= google.golang.org/grpc v1.79.1/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

@@ -5,6 +5,7 @@ import (
"errors" "errors"
"fmt" "fmt"
"slices" "slices"
"time"
"unicode" "unicode"
"github.com/tech/sendico/pkg/auth" "github.com/tech/sendico/pkg/auth"
@@ -13,13 +14,12 @@ import (
"github.com/tech/sendico/pkg/db/account" "github.com/tech/sendico/pkg/db/account"
"github.com/tech/sendico/pkg/db/organization" "github.com/tech/sendico/pkg/db/organization"
"github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/db/policy"
"github.com/tech/sendico/pkg/db/transaction" "github.com/tech/sendico/pkg/db/verification"
"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/model" "github.com/tech/sendico/pkg/model"
"github.com/tech/sendico/pkg/mutil/mzap" "github.com/tech/sendico/pkg/mutil/mzap"
"github.com/tech/sendico/server/interface/middleware" "github.com/tech/sendico/server/interface/middleware"
"github.com/tech/sendico/server/internal/mutil/flrstring"
"go.mongodb.org/mongo-driver/v2/bson" "go.mongodb.org/mongo-driver/v2/bson"
"go.uber.org/zap" "go.uber.org/zap"
) )
@@ -31,9 +31,9 @@ type service struct {
enforcer auth.Enforcer enforcer auth.Enforcer
roleManager management.Role roleManager management.Role
config *middleware.PasswordConfig config *middleware.PasswordConfig
tf transaction.Factory
policyDB policy.DB policyDB policy.DB
vdb verification.DB
} }
func validateUserRequest(u *model.Account) error { func validateUserRequest(u *model.Account) error {
@@ -112,7 +112,6 @@ func (s *service) ValidateAccount(acct *model.Account) error {
return err return err
} }
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength)
return nil return nil
} }
@@ -121,32 +120,54 @@ func (s *service) CreateAccount(
org *model.Organization, org *model.Organization,
acct *model.Account, acct *model.Account,
roleDescID bson.ObjectID, roleDescID bson.ObjectID,
) error { ) (string, error) {
if org == nil { if org == nil {
return merrors.InvalidArgument("Organization must not be nil") return "", merrors.InvalidArgument("Organization must not be nil")
} }
if acct == nil || len(acct.Login) == 0 { if acct == nil || len(acct.Login) == 0 {
return merrors.InvalidArgument("Account must have a non-empty login") return "", merrors.InvalidArgument("Account must have a non-empty login")
} }
if roleDescID == bson.NilObjectID { if roleDescID == bson.NilObjectID {
return merrors.InvalidArgument("Role description must be provided") return "", merrors.InvalidArgument("Role description must be provided")
} }
// 1) Create the account // 1) Create the account
acct.Status = model.AccountPendingVerification
if err := s.accountDB.Create(ctx, acct); err != nil { if err := s.accountDB.Create(ctx, acct); err != nil {
if errors.Is(err, merrors.ErrDataConflict) { if errors.Is(err, merrors.ErrDataConflict) {
s.logger.Info("Username is already taken", zap.String("login", acct.Login)) s.logger.Info("Username is already taken", zap.String("login", acct.Login))
} else { } else {
s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login)) s.logger.Warn("Failed to signup a user", zap.Error(err), zap.String("login", acct.Login))
} }
return err return "", err
} }
// 2) Add to organization // 2) Add to organization
if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil { if err := s.JoinOrganization(ctx, org, acct, roleDescID); err != nil {
s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct)) s.logger.Warn("Failed to register new organization member", zap.Error(err), mzap.StorableRef(acct))
return err return "", err
} }
return nil
// 3) Issue verification token
return s.VerifyAccount(ctx, acct)
}
func (s *service) VerifyAccount(
ctx context.Context,
acct *model.Account,
) (string, error) {
token, err := s.vdb.Create(
ctx,
verification.NewLinkRequest(*acct.GetID(), model.PurposeAccountActivation, "").
WithTTL(time.Duration(time.Hour*24)),
)
if err != nil {
s.logger.Warn("Failed to create verification token for new account", zap.Error(err), mzap.StorableRef(acct))
return "", err
}
return token, nil
} }
func (s *service) DeleteAccount( func (s *service) DeleteAccount(
@@ -157,19 +178,19 @@ func (s *service) DeleteAccount(
// Check if this is the only member in the organization // Check if this is the only member in the organization
if len(org.Members) <= 1 { if len(org.Members) <= 1 {
s.logger.Warn("Cannot delete account - it's the only member in the organization", s.logger.Warn("Cannot delete account - it's the only member in the organization",
mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(org)) mzap.AccRef(accountRef), mzap.StorableRef(org))
return merrors.InvalidArgument("Cannot delete the only member of an organization") return merrors.InvalidArgument("Cannot delete the only member of an organization")
} }
// 1) Remove from organization // 1) Remove from organization
if err := s.RemoveAccountFromOrganization(ctx, org, accountRef); err != nil { if err := s.RemoveAccountFromOrganization(ctx, org, accountRef); err != nil {
s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) s.logger.Warn("Failed to revoke account role", zap.Error(err), mzap.AccRef(accountRef))
return err return err
} }
// 2) Delete the account document // 2) Delete the account document
if err := s.accountDB.Delete(ctx, accountRef); err != nil { if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) s.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
return err return err
} }
return nil return nil
@@ -186,13 +207,13 @@ func (s *service) RemoveAccountFromOrganization(
roles, err := s.enforcer.GetRoles(ctx, accountRef, org.ID) roles, err := s.enforcer.GetRoles(ctx, accountRef, org.ID)
if err != nil { if err != nil {
s.logger.Warn("Failed to fetch account permissions", zap.Error(err), mzap.StorableRef(org), s.logger.Warn("Failed to fetch account permissions", zap.Error(err), mzap.StorableRef(org),
mzap.ObjRef("account_ref", accountRef)) mzap.AccRef(accountRef))
return err return err
} }
for _, role := range roles { for _, role := range roles {
if err := s.roleManager.Revoke(ctx, role.DescriptionRef, accountRef, org.ID); err != nil { if err := s.roleManager.Revoke(ctx, role.DescriptionRef, accountRef, org.ID); err != nil {
s.logger.Warn("Failed to revoke account role", zap.Error(err), s.logger.Warn("Failed to revoke account role", zap.Error(err),
mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("role_ref", role.DescriptionRef)) mzap.AccRef(accountRef), mzap.ObjRef("role_ref", role.DescriptionRef))
return err return err
} }
} }
@@ -201,7 +222,7 @@ func (s *service) RemoveAccountFromOrganization(
// Remove the member by slicing it out // Remove the member by slicing it out
org.Members = append(org.Members[:i], org.Members[i+1:]...) org.Members = append(org.Members[:i], org.Members[i+1:]...)
if err := s.orgDB.Update(ctx, accountRef, org); err != nil { if err := s.orgDB.Update(ctx, accountRef, org); err != nil {
s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) s.logger.Warn("Failed to remove member from organization", zap.Error(err), mzap.AccRef(accountRef))
return err return err
} }
break break
@@ -213,20 +234,24 @@ func (s *service) RemoveAccountFromOrganization(
func (s *service) ResetPassword( func (s *service) ResetPassword(
ctx context.Context, ctx context.Context,
acct *model.Account, acct *model.Account,
) error { ) (string, error) {
acct.ResetPasswordToken = flrstring.CreateRandString(s.config.TokenLength) return s.vdb.Create(
return s.accountDB.Update(ctx, acct) ctx,
verification.NewOTPRequest(*acct.GetID(), model.PurposePasswordReset, "").
WithTTL(time.Duration(time.Hour*1)),
)
} }
func (s *service) UpdateLogin( func (s *service) UpdateLogin(
ctx context.Context, ctx context.Context,
acct *model.Account, acct *model.Account,
newLogin string, newLogin string,
) error { ) (string, error) {
acct.EmailBackup = acct.Login return s.vdb.Create(
acct.Login = newLogin ctx,
acct.VerifyToken = flrstring.CreateRandString(s.config.TokenLength) verification.NewOTPRequest(*acct.GetID(), model.PurposeEmailChange, newLogin).
return s.accountDB.Update(ctx, acct) WithTTL(time.Duration(time.Hour*1)),
)
} }
func (s *service) JoinOrganization( func (s *service) JoinOrganization(
@@ -311,27 +336,19 @@ func (s *service) DeleteOrganization(
s.logger.Info("Starting organization deletion", mzap.StorableRef(org)) s.logger.Info("Starting organization deletion", mzap.StorableRef(org))
// Use transaction to ensure atomicity // Use transaction to ensure atomicity
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) { // 8. Delete all roles and role descriptions in the organization
// 8. Delete all roles and role descriptions in the organization if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil {
if err := s.deleteOrganizationRoles(ctx, org.ID); err != nil { return err
return nil, err }
}
// 9. Delete all policies in the organization // 9. Delete all policies in the organization
if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil { if err := s.deleteOrganizationPolicies(ctx, org.ID); err != nil {
return nil, err return err
} }
// 10. Finally, delete the organization itself // 10. Finally, delete the organization itself
if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil { if err := s.orgDB.Delete(ctx, bson.NilObjectID, org.ID); err != nil {
s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org)) s.logger.Warn("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
return nil, err
}
return nil, nil
})
if err != nil {
s.logger.Error("Failed to delete organization", zap.Error(err), mzap.StorableRef(org))
return err return err
} }
@@ -345,29 +362,20 @@ func (s *service) DeleteAll(
accountRef bson.ObjectID, accountRef bson.ObjectID,
) error { ) error {
s.logger.Info("Starting complete deletion (organization + account)", s.logger.Info("Starting complete deletion (organization + account)",
mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef)) mzap.StorableRef(org), mzap.AccRef(accountRef))
// Use transaction to ensure atomicity // 1. First delete the organization and all its data
_, err := s.tf.CreateTransaction().Execute(ctx, func(ctx context.Context) (any, error) { if err := s.DeleteOrganization(ctx, org); err != nil {
// 1. First delete the organization and all its data
if err := s.DeleteOrganization(ctx, org); err != nil {
return nil, err
}
// 2. Then delete the account
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.ObjRef("account_ref", accountRef))
return nil, err
}
return nil, nil
})
if err != nil {
s.logger.Error("Failed to delete all data", zap.Error(err), mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef))
return err return err
} }
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.ObjRef("account_ref", accountRef)) // 2. Then delete the account
if err := s.accountDB.Delete(ctx, accountRef); err != nil {
s.logger.Warn("Failed to delete account", zap.Error(err), mzap.AccRef(accountRef))
return err
}
s.logger.Info("Complete deletion successful", mzap.StorableRef(org), mzap.AccRef(accountRef))
return nil return nil
} }
@@ -390,7 +398,6 @@ func NewAccountService(
enforcer: enforcer, enforcer: enforcer,
roleManager: ra, roleManager: ra,
config: config, config: config,
tf: dbf.TransactionFactory(),
} }
var err error var err error
if res.accountDB, err = dbf.NewAccountDB(); err != nil { if res.accountDB, err = dbf.NewAccountDB(); err != nil {
@@ -407,6 +414,9 @@ func NewAccountService(
logger.Warn("Failed to create policies database", zap.Error(err)) logger.Warn("Failed to create policies database", zap.Error(err))
return nil, err return nil, err
} }
if res.vdb, err = dbf.NewVerificationsDB(); err != nil {
logger.Warn("Failed to create verification database", zap.Error(err))
return nil, err
}
return res, nil return res, nil
} }

View File

@@ -113,8 +113,6 @@ func TestValidateAccount(t *testing.T) {
// Password should be hashed after validation // Password should be hashed after validation
assert.NotEqual(t, originalPassword, account.Password) assert.NotEqual(t, originalPassword, account.Password)
assert.NotEmpty(t, account.VerifyToken)
assert.Equal(t, config.TokenLength, len(account.VerifyToken))
}) })
t.Run("AccountMissingName", func(t *testing.T) { t.Run("AccountMissingName", func(t *testing.T) {
@@ -245,54 +243,3 @@ func TestPasswordConfiguration(t *testing.T) {
}) })
} }
// TestTokenGeneration verifies that verification tokens are generated with correct length
func TestTokenGeneration(t *testing.T) {
testCases := []struct {
name string
tokenLength int
}{
{"Short", 8},
{"Medium", 32},
{"Long", 64},
}
for _, tc := range testCases {
t.Run(tc.name, func(t *testing.T) {
config := &apiconfig.PasswordConfig{
Check: apiconfig.PasswordChecks{
MinLength: 8,
Digit: true,
Upper: true,
Lower: true,
Special: true,
},
TokenLength: tc.tokenLength,
}
logger := zap.NewNop() // Use no-op logger for tests
service := &service{
config: config,
logger: logger,
}
account := &model.Account{
AccountPublic: model.AccountPublic{
AccountBase: model.AccountBase{
Describable: model.Describable{
Name: "Test User",
},
},
UserDataBase: model.UserDataBase{
Login: "test@example.com",
},
},
Password: "TestPassword123!",
}
err := service.ValidateAccount(account)
require.NoError(t, err)
assert.Equal(t, tc.tokenLength, len(account.VerifyToken))
})
}
}

View File

@@ -35,7 +35,7 @@ type AccountService interface {
ResetPassword( ResetPassword(
ctx context.Context, ctx context.Context,
acct *model.Account, acct *model.Account,
) error ) (verificationToken string, err error)
// CreateAccount will: // CreateAccount will:
// 1) create the account // 1) create the account
@@ -46,7 +46,12 @@ type AccountService interface {
org *model.Organization, org *model.Organization,
acct *model.Account, acct *model.Account,
roleDescID bson.ObjectID, roleDescID bson.ObjectID,
) error ) (verificationToken string, err error)
VerifyAccount(
ctx context.Context,
acct *model.Account,
) (verificationToken string, err error)
JoinOrganization( JoinOrganization(
ctx context.Context, ctx context.Context,
@@ -59,7 +64,7 @@ type AccountService interface {
ctx context.Context, ctx context.Context,
acct *model.Account, acct *model.Account,
newLogin string, newLogin string,
) error ) (verificationToken string, err error)
// DeleteAccount deletes the account and removes it from the org. // DeleteAccount deletes the account and removes it from the org.
DeleteAccount( DeleteAccount(

View File

@@ -1,6 +1,7 @@
package api package api
import ( import (
"github.com/tech/sendico/pkg/vault/kv"
mwa "github.com/tech/sendico/server/interface/middleware" mwa "github.com/tech/sendico/server/interface/middleware"
fsc "github.com/tech/sendico/server/interface/services/fileservice/config" fsc "github.com/tech/sendico/server/interface/services/fileservice/config"
) )
@@ -11,6 +12,9 @@ type Config struct {
ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"` ChainGateway *ChainGatewayConfig `yaml:"chain_gateway"`
Ledger *LedgerConfig `yaml:"ledger"` Ledger *LedgerConfig `yaml:"ledger"`
PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"` PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"`
PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"`
PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"`
Callbacks *CallbacksConfig `yaml:"callbacks"`
} }
type ChainGatewayConfig struct { type ChainGatewayConfig struct {
@@ -43,3 +47,12 @@ type PaymentOrchestratorConfig struct {
CallTimeoutSeconds int `yaml:"call_timeout_seconds"` CallTimeoutSeconds int `yaml:"call_timeout_seconds"`
Insecure bool `yaml:"insecure"` Insecure bool `yaml:"insecure"`
} }
type CallbacksConfig struct {
DefaultEventTypes []string `yaml:"default_event_types"`
DefaultStatus string `yaml:"default_status"`
SecretPathPrefix string `yaml:"secret_path_prefix"`
SecretField string `yaml:"secret_field"`
SecretLengthBytes int `yaml:"secret_length_bytes"`
Vault kv.Config `yaml:"vault"`
}

View File

@@ -5,4 +5,5 @@ import "github.com/tech/sendico/pkg/model"
type Login struct { type Login struct {
model.SessionIdentifier `json:",inline"` model.SessionIdentifier `json:",inline"`
model.LoginData `json:"login"` model.LoginData `json:"login"`
ClientSecret string `json:"clientSecret,omitempty"`
} }

View File

@@ -1,6 +1,8 @@
package srequest package srequest
import ( import (
"strings"
"github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/merrors"
) )
@@ -23,8 +25,7 @@ type QuotePayment struct {
} }
func (r *QuotePayment) Validate() error { func (r *QuotePayment) Validate() error {
// base checks if err := validateQuoteIdempotency(r.PreviewOnly, r.IdempotencyKey); err != nil {
if err := r.PaymentBase.Validate(); err != nil {
return err return err
} }
@@ -43,7 +44,7 @@ type QuotePayments struct {
} }
func (r *QuotePayments) Validate() error { func (r *QuotePayments) Validate() error {
if err := r.PaymentBase.Validate(); err != nil { if err := validateQuoteIdempotency(r.PreviewOnly, r.IdempotencyKey); err != nil {
return err return err
} }
if len(r.Intents) == 0 { if len(r.Intents) == 0 {
@@ -57,10 +58,25 @@ func (r *QuotePayments) Validate() error {
return nil return nil
} }
func validateQuoteIdempotency(previewOnly bool, idempotencyKey string) error {
key := strings.TrimSpace(idempotencyKey)
if previewOnly {
if key != "" {
return merrors.InvalidArgument("previewOnly requests must not include idempotencyKey", "idempotencyKey")
}
return nil
}
if key == "" {
return merrors.InvalidArgument("idempotencyKey is required", "idempotencyKey")
}
return nil
}
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 {
@@ -91,14 +107,20 @@ 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 {
if r == nil {
return merrors.InvalidArgument("request is required")
}
if err := r.PaymentBase.Validate(); err != nil { if err := r.PaymentBase.Validate(); err != nil {
return err return err
} }
r.QuoteRef = strings.TrimSpace(r.QuoteRef)
if r.QuoteRef == "" { if r.QuoteRef == "" {
return merrors.InvalidArgument("quoteRef is required", "quoteRef") return merrors.InvalidArgument("quoteRef is required", "quoteRef")
} }

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