From 62a6631b9a75e5276095b61e25fd3d4a8bbea164 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 7 Nov 2025 18:35:26 +0100 Subject: [PATCH] service backend --- .gitignore | 1 + api/billing/fees/.air.toml | 32 + api/billing/fees/.gitignore | 3 + api/billing/fees/config.yml | 40 + api/billing/fees/env/.gitignore | 1 + api/billing/fees/go.mod | 54 + api/billing/fees/go.sum | 225 +++ .../fees/internal/appversion/version.go | 28 + .../internal/server/internal/serverimp.go | 163 ++ api/billing/fees/internal/server/server.go | 12 + .../fees/internal/service/fees/calculator.go | 449 ++++++ .../fees/internal/service/fees/metrics.go | 71 + .../fees/internal/service/fees/options.go | 37 + .../fees/internal/service/fees/service.go | 322 ++++ .../internal/service/fees/service_test.go | 476 ++++++ api/billing/fees/main.go | 17 + api/billing/fees/storage/model/plan.go | 62 + api/billing/fees/storage/mongo/repository.go | 69 + api/billing/fees/storage/mongo/store/plans.go | 144 ++ api/billing/fees/storage/storage.go | 36 + api/chain/gateway/.air.toml | 32 + api/chain/gateway/.gitignore | 3 + api/chain/gateway/client/client.go | 148 ++ api/chain/gateway/client/config.go | 20 + api/chain/gateway/client/fake.go | 83 + api/chain/gateway/config.yml | 57 + api/chain/gateway/go.mod | 90 ++ api/chain/gateway/go.sum | 379 +++++ .../gateway/internal/appversion/version.go | 27 + .../gateway/internal/keymanager/config.go | 13 + .../gateway/internal/keymanager/keymanager.go | 23 + .../internal/keymanager/vault/manager.go | 269 ++++ .../internal/server/internal/serverimp.go | 259 +++ api/chain/gateway/internal/server/server.go | 12 + .../service/gateway/conversion_helpers.go | 21 + .../internal/service/gateway/executor.go | 385 +++++ .../internal/service/gateway/metrics.go | 65 + .../internal/service/gateway/options.go | 90 ++ .../internal/service/gateway/service.go | 214 +++ .../internal/service/gateway/service_test.go | 556 +++++++ .../service/gateway/transfer_execution.go | 99 ++ .../service/gateway/transfer_handlers.go | 309 ++++ .../service/gateway/wallet_handlers.go | 213 +++ api/chain/gateway/main.go | 17 + api/chain/gateway/storage/model/deposit.go | 54 + api/chain/gateway/storage/model/transfer.go | 91 ++ api/chain/gateway/storage/model/wallet.go | 90 ++ api/chain/gateway/storage/mongo/repository.go | 98 ++ .../gateway/storage/mongo/store/deposits.go | 161 ++ .../gateway/storage/mongo/store/transfers.go | 200 +++ .../gateway/storage/mongo/store/wallets.go | 236 +++ api/chain/gateway/storage/storage.go | 53 + api/fx/ingestor/.DS_Store | Bin 0 -> 6148 bytes api/fx/ingestor/.air.toml | 32 + api/fx/ingestor/.gitignore | 3 + api/fx/ingestor/config.yml | 43 + api/fx/ingestor/env/.gitignore | 1 + api/fx/ingestor/go.mod | 55 + api/fx/ingestor/go.sum | 225 +++ .../ingestor/internal/appversion/version.go | 27 + api/fx/ingestor/internal/config/config.go | 147 ++ api/fx/ingestor/internal/config/market.go | 24 + api/fx/ingestor/internal/config/metrics.go | 6 + api/fx/ingestor/internal/fmerrors/market.go | 35 + api/fx/ingestor/internal/ingestor/metrics.go | 84 + api/fx/ingestor/internal/ingestor/service.go | 207 +++ .../internal/ingestor/service_test.go | 237 +++ .../internal/market/binance/connector.go | 139 ++ .../internal/market/coingecko/connector.go | 222 +++ .../internal/market/common/settings.go | 46 + api/fx/ingestor/internal/market/factory.go | 55 + api/fx/ingestor/internal/metrics/server.go | 134 ++ api/fx/ingestor/internal/model/connector.go | 30 + api/fx/ingestor/internal/model/ticker.go | 9 + .../ingestor/internal/signalctx/signalctx.go | 14 + api/fx/ingestor/main.go | 55 + api/fx/oracle/.air.toml | 32 + api/fx/oracle/.gitignore | 3 + api/fx/oracle/client/client.go | 252 +++ api/fx/oracle/client/client_test.go | 116 ++ api/fx/oracle/client/config.go | 20 + api/fx/oracle/client/fake.go | 60 + api/fx/oracle/config.yml | 34 + api/fx/oracle/env/.gitignore | 1 + api/fx/oracle/go.mod | 54 + api/fx/oracle/go.sum | 225 +++ api/fx/oracle/internal/appversion/version.go | 27 + .../internal/server/internal/serverimp.go | 101 ++ api/fx/oracle/internal/server/server.go | 11 + .../internal/service/oracle/calculator.go | 223 +++ .../oracle/internal/service/oracle/cross.go | 221 +++ api/fx/oracle/internal/service/oracle/math.go | 67 + .../oracle/internal/service/oracle/metrics.go | 65 + .../oracle/internal/service/oracle/service.go | 402 +++++ .../internal/service/oracle/service_test.go | 467 ++++++ .../internal/service/oracle/transform.go | 126 ++ api/fx/oracle/main.go | 17 + api/fx/storage/.gitignore | 2 + api/fx/storage/go.mod | 32 + api/fx/storage/go.sum | 177 +++ api/fx/storage/model/cross.go | 18 + api/fx/storage/model/currency.go | 27 + api/fx/storage/model/pair.go | 26 + api/fx/storage/model/quote.go | 63 + api/fx/storage/model/rate.go | 34 + api/fx/storage/model/types.go | 68 + api/fx/storage/mongo/repository.go | 115 ++ api/fx/storage/mongo/store/currency.go | 113 ++ api/fx/storage/mongo/store/currency_test.go | 104 ++ api/fx/storage/mongo/store/pair.go | 111 ++ api/fx/storage/mongo/store/pair_test.go | 101 ++ api/fx/storage/mongo/store/quotes.go | 198 +++ api/fx/storage/mongo/store/quotes_test.go | 184 +++ api/fx/storage/mongo/store/rates.go | 127 ++ api/fx/storage/mongo/store/rates_test.go | 87 + .../mongo/store/testing_helpers_test.go | 189 +++ api/fx/storage/mongo/transaction.go | 38 + api/fx/storage/storage.go | 53 + api/ledger/.air.toml | 32 + api/ledger/.gitignore | 3 + api/ledger/METRICS.md | 306 ++++ api/ledger/client/client.go | 142 ++ api/ledger/client/config.go | 20 + api/ledger/client/fake.go | 75 + api/ledger/config.yml | 38 + api/ledger/env/.gitignore | 1 + api/ledger/go.mod | 55 + api/ledger/go.sum | 227 +++ api/ledger/internal/appversion/version.go | 27 + api/ledger/internal/model/account.go | 133 ++ api/ledger/internal/model/balance.go | 19 + api/ledger/internal/model/jentry.go | 46 + api/ledger/internal/model/outbox.go | 35 + api/ledger/internal/model/ownership.go | 57 + api/ledger/internal/model/party.go | 76 + api/ledger/internal/model/pline.go | 37 + api/ledger/internal/model/util.go | 10 + api/ledger/internal/model/validation.go | 31 + .../internal/server/internal/serverimp.go | 160 ++ api/ledger/internal/server/server.go | 11 + .../internal/service/ledger/accounts.go | 208 +++ .../internal/service/ledger/accounts_test.go | 168 ++ api/ledger/internal/service/ledger/helpers.go | 166 ++ .../internal/service/ledger/helpers_test.go | 417 +++++ api/ledger/internal/service/ledger/metrics.go | 144 ++ .../service/ledger/outbox_publisher.go | 206 +++ .../service/ledger/outbox_publisher_test.go | 142 ++ api/ledger/internal/service/ledger/posting.go | 239 +++ .../internal/service/ledger/posting_debit.go | 233 +++ .../internal/service/ledger/posting_fx.go | 254 +++ .../service/ledger/posting_support.go | 228 +++ .../service/ledger/posting_support_test.go | 282 ++++ .../service/ledger/posting_transfer.go | 238 +++ api/ledger/internal/service/ledger/queries.go | 269 ++++ .../internal/service/ledger/queries_test.go | 99 ++ api/ledger/internal/service/ledger/service.go | 357 +++++ api/ledger/main.go | 17 + api/ledger/storage/model/account.go | 25 + api/ledger/storage/model/account_balance.go | 27 + api/ledger/storage/model/journal_entry.go | 26 + api/ledger/storage/model/outbox.go | 27 + api/ledger/storage/model/posting_line.go | 24 + api/ledger/storage/model/types.go | 78 + api/ledger/storage/mongo/repository.go | 132 ++ api/ledger/storage/mongo/store/accounts.go | 220 +++ .../storage/mongo/store/accounts_test.go | 436 +++++ api/ledger/storage/mongo/store/balances.go | 115 ++ .../storage/mongo/store/balances_test.go | 285 ++++ .../storage/mongo/store/journal_entries.go | 160 ++ .../mongo/store/journal_entries_test.go | 299 ++++ api/ledger/storage/mongo/store/outbox.go | 155 ++ api/ledger/storage/mongo/store/outbox_test.go | 336 ++++ .../storage/mongo/store/posting_lines.go | 138 ++ .../storage/mongo/store/posting_lines_test.go | 276 ++++ .../mongo/store/testing_helpers_test.go | 137 ++ api/ledger/storage/mongo/transaction.go | 38 + api/ledger/storage/repository.go | 14 + api/ledger/storage/storage.go | 61 + api/payments/orchestrator/.gitignore | 3 + api/payments/orchestrator/client/client.go | 148 ++ api/payments/orchestrator/client/config.go | 20 + api/payments/orchestrator/client/fake.go | 83 + api/payments/orchestrator/env/.gitignore | 1 + api/payments/orchestrator/go.mod | 60 + api/payments/orchestrator/go.sum | 225 +++ .../internal/service/orchestrator/convert.go | 426 +++++ .../service/orchestrator/execution.go | 495 ++++++ .../internal/service/orchestrator/helpers.go | 295 ++++ .../service/orchestrator/internal_helpers.go | 71 + .../internal/service/orchestrator/metrics.go | 65 + .../internal/service/orchestrator/options.go | 87 + .../internal/service/orchestrator/service.go | 504 ++++++ .../service/orchestrator/service_test.go | 290 ++++ .../orchestrator/storage/model/payment.go | 226 +++ .../orchestrator/storage/mongo/repository.go | 68 + .../storage/mongo/store/payments.go | 266 ++++ api/payments/orchestrator/storage/storage.go | 37 + api/pkg/.DS_Store | Bin 0 -> 6148 bytes api/pkg/.gitignore | 6 + api/pkg/api/http/methods.go | 36 + api/pkg/api/http/response/response.go | 205 +++ api/pkg/api/http/response/result.go | 19 + api/pkg/api/http/status.go | 8 + api/pkg/api/routers/grpc.go | 61 + api/pkg/api/routers/gsresponse/response.go | 149 ++ .../api/routers/gsresponse/response_test.go | 75 + api/pkg/api/routers/health.go | 17 + api/pkg/api/routers/health/status.go | 10 + .../api/routers/internal/grpcimp/config.go | 18 + .../api/routers/internal/grpcimp/metrics.go | 103 ++ .../api/routers/internal/grpcimp/options.go | 14 + .../api/routers/internal/grpcimp/router.go | 293 ++++ .../routers/internal/grpcimp/router_test.go | 150 ++ .../api/routers/internal/healthimp/health.go | 45 + .../api/routers/internal/healthimp/status.go | 38 + .../routers/internal/messagingimp/consumer.go | 66 + .../internal/messagingimp/messsaging.go | 67 + api/pkg/api/routers/messaging.go | 16 + api/pkg/auth/USAGE.md | 202 +++ api/pkg/auth/anyobject/anyobject.go | 3 + api/pkg/auth/archivable.go | 35 + api/pkg/auth/archivableimp.go | 107 ++ api/pkg/auth/config.go | 12 + api/pkg/auth/customizable/customizable.go | 8 + api/pkg/auth/customizable/manager.go | 8 + api/pkg/auth/db.go | 38 + api/pkg/auth/dbab.go | 51 + api/pkg/auth/dbimp.go | 319 ++++ api/pkg/auth/dbimpab.go | 420 +++++ api/pkg/auth/dbimpab_test.go | 81 + api/pkg/auth/enforcer.go | 32 + api/pkg/auth/factory.go | 52 + api/pkg/auth/helper.go | 61 + api/pkg/auth/indexable.go | 29 + api/pkg/auth/indexableimp.go | 182 +++ api/pkg/auth/internal/casbin/action.go | 23 + api/pkg/auth/internal/casbin/config/config.go | 126 ++ api/pkg/auth/internal/casbin/enforcer.go | 206 +++ api/pkg/auth/internal/casbin/factory.go | 34 + api/pkg/auth/internal/casbin/logger.go | 61 + api/pkg/auth/internal/casbin/manager.go | 54 + api/pkg/auth/internal/casbin/models/auth.conf | 54 + api/pkg/auth/internal/casbin/permissions.go | 167 ++ api/pkg/auth/internal/casbin/role.go | 209 +++ .../casbin/serialization/internal/policy.go | 81 + .../casbin/serialization/internal/role.go | 57 + .../internal/casbin/serialization/policy.go | 12 + .../internal/casbin/serialization/role.go | 12 + .../casbin/serialization/serializer.go | 10 + api/pkg/auth/internal/native/db/policies.go | 151 ++ api/pkg/auth/internal/native/db/roles.go | 99 ++ api/pkg/auth/internal/native/dbpolicies.go | 27 + api/pkg/auth/internal/native/dbroles.go | 24 + api/pkg/auth/internal/native/enforcer.go | 256 +++ api/pkg/auth/internal/native/enforcer_test.go | 747 +++++++++ api/pkg/auth/internal/native/manager.go | 51 + api/pkg/auth/internal/native/native.test | Bin 0 -> 14400930 bytes .../internal/native/nstructures/policies.go | 17 + .../auth/internal/native/nstructures/role.go | 15 + api/pkg/auth/internal/native/permission.go | 101 ++ api/pkg/auth/internal/native/role.go | 142 ++ api/pkg/auth/management/permission.go | 27 + api/pkg/auth/management/role.go | 41 + api/pkg/auth/manager.go | 15 + api/pkg/auth/provider.go | 14 + api/pkg/auth/taggable.go | 43 + api/pkg/auth/taggableimp.go | 302 ++++ api/pkg/clock/clock.go | 21 + api/pkg/db/account/account.go | 17 + api/pkg/db/config.go | 11 + api/pkg/db/connection.go | 65 + api/pkg/db/factory.go | 41 + api/pkg/db/indexable/indexable.go | 12 + api/pkg/db/internal/mongo/accountdb/db.go | 30 + api/pkg/db/internal/mongo/accountdb/token.go | 13 + api/pkg/db/internal/mongo/accountdb/user.go | 21 + .../internal/mongo/archivable/archivable.go | 99 ++ .../mongo/archivable/archivable_test.go | 175 +++ api/pkg/db/internal/mongo/db.go | 257 +++ api/pkg/db/internal/mongo/indexable/README.md | 144 ++ api/pkg/db/internal/mongo/indexable/USAGE.md | 174 ++ .../db/internal/mongo/indexable/examples.go | 69 + .../db/internal/mongo/indexable/indexable.go | 122 ++ .../mongo/indexable/indexable_test.go | 314 ++++ .../db/internal/mongo/invitationdb/accept.go | 12 + .../internal/mongo/invitationdb/archived.go | 49 + .../db/internal/mongo/invitationdb/cascade.go | 24 + api/pkg/db/internal/mongo/invitationdb/db.go | 53 + .../db/internal/mongo/invitationdb/decline.go | 12 + .../internal/mongo/invitationdb/getpublic.go | 121 ++ .../db/internal/mongo/invitationdb/list.go | 28 + .../mongo/invitationdb/updatestatus.go | 26 + api/pkg/db/internal/mongo/mongo.go | 22 + .../internal/mongo/organizationdb/archived.go | 32 + .../internal/mongo/organizationdb/cascade.go | 23 + .../internal/mongo/organizationdb/create.go | 19 + .../db/internal/mongo/organizationdb/db.go | 34 + .../db/internal/mongo/organizationdb/get.go | 12 + .../db/internal/mongo/organizationdb/list.go | 16 + .../db/internal/mongo/organizationdb/owned.go | 14 + .../mongo/organizationdb/setarchived_test.go | 562 +++++++ api/pkg/db/internal/mongo/policiesdb/all.go | 20 + .../db/internal/mongo/policiesdb/builtin.go | 13 + api/pkg/db/internal/mongo/policiesdb/db.go | 21 + .../db/internal/mongo/policiesdb/db_test.go | 353 +++++ .../db/internal/mongo/policiesdb/policies.go | 18 + .../internal/mongo/refreshtokensdb/client.go | 12 + .../db/internal/mongo/refreshtokensdb/crud.go | 122 ++ .../db/internal/mongo/refreshtokensdb/db.go | 62 + .../internal/mongo/refreshtokensdb/fields.go | 10 + .../internal/mongo/refreshtokensdb/filters.go | 25 + .../refreshtokensdb/refreshtokensdb_test.go | 639 ++++++++ .../internal/mongo/refreshtokensdb/revoke.go | 24 + .../repositoryimp/builderimp/accumulator.go | 90 ++ .../mongo/repositoryimp/builderimp/alias.go | 102 ++ .../mongo/repositoryimp/builderimp/array.go | 27 + .../repositoryimp/builderimp/expression.go | 108 ++ .../mongo/repositoryimp/builderimp/field.go | 71 + .../mongo/repositoryimp/builderimp/func.go | 137 ++ .../repositoryimp/builderimp/gaccumulator.go | 35 + .../mongo/repositoryimp/builderimp/patch.go | 60 + .../repositoryimp/builderimp/pipeline.go | 131 ++ .../repositoryimp/builderimp/pipeline_test.go | 563 +++++++ .../repositoryimp/builderimp/projection.go | 97 ++ .../mongo/repositoryimp/builderimp/query.go | 156 ++ .../mongo/repositoryimp/builderimp/value.go | 17 + .../db/internal/mongo/repositoryimp/index.go | 50 + .../mongo/repositoryimp/repository.go | 250 +++ .../repository_comprehensive_test.go | 577 +++++++ .../repository_insertmany_test.go | 153 ++ .../repositoryimp/repository_patch_test.go | 233 +++ .../mongo/repositoryimp/repository_test.go | 188 +++ api/pkg/db/internal/mongo/rolesdb/db.go | 21 + api/pkg/db/internal/mongo/rolesdb/list.go | 15 + api/pkg/db/internal/mongo/rolesdb/roles.go | 15 + .../internal/mongo/transactionimp/factory.go | 18 + .../mongo/transactionimp/transaction.go | 30 + .../db/internal/mongo/tseriesimp/tseries.go | 118 ++ api/pkg/db/invitation/invitation.go | 19 + api/pkg/db/organization/organization.go | 17 + api/pkg/db/policy/policy.go | 17 + api/pkg/db/refreshtokens/refreshtokens.go | 17 + api/pkg/db/repository/abfilter.go | 78 + api/pkg/db/repository/builder/accumulator.go | 11 + api/pkg/db/repository/builder/alias.go | 8 + api/pkg/db/repository/builder/array.go | 7 + api/pkg/db/repository/builder/expression.go | 5 + api/pkg/db/repository/builder/field.go | 7 + api/pkg/db/repository/builder/keyword.go | 16 + api/pkg/db/repository/builder/operators.go | 57 + api/pkg/db/repository/builder/patch.go | 16 + api/pkg/db/repository/builder/pipeline.go | 24 + api/pkg/db/repository/builder/projection.go | 7 + api/pkg/db/repository/builder/query.go | 24 + api/pkg/db/repository/builder/unwind.go | 23 + api/pkg/db/repository/builder/value.go | 5 + api/pkg/db/repository/builders.go | 273 ++++ api/pkg/db/repository/cursor.go | 19 + api/pkg/db/repository/decoder/decoder.go | 5 + api/pkg/db/repository/filter_factory_test.go | 93 ++ api/pkg/db/repository/index/index.go | 21 + api/pkg/db/repository/index/types.go | 36 + api/pkg/db/repository/repository.go | 46 + api/pkg/db/role/role.go | 15 + api/pkg/db/storable/id.go | 39 + api/pkg/db/storable/ref.go | 11 + api/pkg/db/storable/storable.go | 10 + api/pkg/db/tag/tag.go | 16 + api/pkg/db/template/interface.go | 21 + api/pkg/db/template/template.go | 104 ++ api/pkg/db/template/tseries.go | 14 + api/pkg/db/transaction/factory.go | 5 + api/pkg/db/transaction/transaction.go | 11 + api/pkg/db/tseries/factory.go | 13 + api/pkg/db/tseries/options/options.go | 23 + api/pkg/db/tseries/point/interface.go | 14 + api/pkg/db/tseries/template.go | 1 + api/pkg/db/tseries/tseries.go | 29 + api/pkg/decimal/money.go | 129 ++ api/pkg/decimal/rational.go | 161 ++ api/pkg/decimal/rounding.go | 29 + api/pkg/domainprovider/domain_provider.go | 15 + api/pkg/domainprovider/imp/domain_provider.go | 35 + api/pkg/go.mod | 98 ++ api/pkg/go.sum | 295 ++++ api/pkg/localization/locale.go | 6 + api/pkg/localization/localization.go | 7 + api/pkg/merrors/errors.go | 65 + api/pkg/messaging/broker/broker.go | 12 + api/pkg/messaging/config.go | 14 + api/pkg/messaging/consumer.go | 6 + api/pkg/messaging/envelope/envelope.go | 28 + api/pkg/messaging/factory.go | 19 + api/pkg/messaging/handler.go | 9 + api/pkg/messaging/inprocess/inprocess.go | 18 + api/pkg/messaging/internal/.gitignore | 1 + .../messaging/internal/envelope/envelope.go | 101 ++ .../messaging/internal/inprocess/broker.go | 87 + .../internal/inprocess/config/config.go | 5 + api/pkg/messaging/internal/natsb/NATS.go | 86 + api/pkg/messaging/internal/natsb/broker.go | 113 ++ .../messaging/internal/natsb/config/config.go | 12 + .../messaging/internal/natsb/subscription.go | 78 + .../notifications/account/notification.go | 37 + .../notifications/account/password_reset.go | 40 + .../account/password_reset_processor.go | 57 + .../notifications/account/processor.go | 57 + .../notifications/invitation/processor.go | 63 + .../notification/notification.go | 44 + .../notifications/notification/processor.go | 53 + .../internal/notifications/object/object.go | 46 + .../notifications/object/processor.go | 55 + .../messaging/internal/producer/producer.go | 26 + api/pkg/messaging/message/message.go | 5 + api/pkg/messaging/messaging.go | 10 + api/pkg/messaging/natsb/nats.go | 18 + .../notifications/account/created.go | 16 + .../account/handler/interface.go | 11 + .../notifications/account/password_reset.go | 24 + .../notifications/account/processor.go | 14 + .../invitation/handler/interface.go | 9 + .../notifications/invitation/invitation.go | 43 + .../notifications/invitation/processor.go | 30 + .../notification/handler/interface.go | 9 + .../notifications/notification/sent.go | 11 + .../notifications/object/handler/interface.go | 16 + .../messaging/notifications/object/object.go | 46 + .../notifications/object/processor.go | 19 + .../notifications/processor/envelope.go | 13 + api/pkg/messaging/producer.go | 7 + api/pkg/messaging/producer/producer.go | 12 + api/pkg/mlogger/factory/mlogger.go | 10 + api/pkg/mlogger/internal/mlogger/mlogger.go | 23 + api/pkg/mlogger/logger.go | 7 + api/pkg/model/account.go | 84 + api/pkg/model/ampli.go | 5 + api/pkg/model/archivable.go | 18 + api/pkg/model/attachment.go | 8 + api/pkg/model/auth.go | 84 + api/pkg/model/automation.go | 15 + api/pkg/model/client.go | 24 + api/pkg/model/colorable.go | 6 + api/pkg/model/comment.go | 35 + api/pkg/model/commentp.go | 8 + api/pkg/model/currency.go | 27 + api/pkg/model/customizable.go | 13 + api/pkg/model/describable.go | 11 + api/pkg/model/dzone.go | 7 + api/pkg/model/fconfig.go | 8 + api/pkg/model/filter.go | 31 + api/pkg/model/indexable.go | 12 + api/pkg/model/internal/notificationevent.go | 83 + api/pkg/model/invitation.go | 71 + api/pkg/model/invoice.go | 30 + api/pkg/model/link.go | 11 + api/pkg/model/notification/notification.go | 14 + api/pkg/model/notificationevent.go | 91 ++ api/pkg/model/nresult.go | 9 + api/pkg/model/object.go | 20 + api/pkg/model/opresult.go | 6 + api/pkg/model/organization.go | 44 + api/pkg/model/pbinding.go | 32 + api/pkg/model/permission.go | 33 + api/pkg/model/pfilter.go | 24 + api/pkg/model/priority.go | 24 + api/pkg/model/project.go | 61 + api/pkg/model/property.go | 671 ++++++++ api/pkg/model/reaction.go | 23 + api/pkg/model/refresh.go | 26 + api/pkg/model/sessionid.go | 6 + api/pkg/model/status.go | 26 + api/pkg/model/step.go | 20 + api/pkg/model/tag.go | 23 + api/pkg/model/task.go | 26 + api/pkg/model/team.go | 19 + api/pkg/model/tenant.go | 15 + api/pkg/model/userdata.go | 30 + api/pkg/model/value.go | 751 +++++++++ api/pkg/model/value_test.go | 1397 +++++++++++++++++ api/pkg/model/viewcursor.go | 8 + api/pkg/model/workflow.go | 19 + api/pkg/model/workspace.go | 17 + api/pkg/mservice/mservice.go | 10 + api/pkg/mservice/services.go | 72 + api/pkg/mutil/config/param.go | 74 + api/pkg/mutil/db/archive.go | 37 + api/pkg/mutil/db/array.go | 26 + api/pkg/mutil/db/auth/accountbound.go | 89 ++ api/pkg/mutil/db/auth/protected.go | 58 + api/pkg/mutil/db/db.go | 20 + api/pkg/mutil/duration/duration.go | 7 + api/pkg/mutil/fr/fr.go | 32 + api/pkg/mutil/helpers/accountmanager.go | 22 + api/pkg/mutil/helpers/factory.go | 27 + api/pkg/mutil/helpers/integration_test.go | 128 ++ .../mutil/helpers/internal/accountmanager.go | 136 ++ .../helpers/internal/simple_internal_test.go | 56 + .../internal/task_manager_business_test.go | 267 ++++ api/pkg/mutil/helpers/internal/taskmanager.go | 110 ++ api/pkg/mutil/helpers/simple_test.go | 67 + api/pkg/mutil/helpers/taskmanager.go | 27 + api/pkg/mutil/helpers/taskmanager_factory.go | 11 + api/pkg/mutil/http/http.go | 69 + api/pkg/mutil/imagewriter/imagewriter.go | 15 + api/pkg/mutil/mzap/envelope.go | 24 + api/pkg/mutil/mzap/object.go | 15 + api/pkg/mutil/reorder/reorder.go | 42 + api/pkg/mutil/time/go/gotime.go | 15 + api/pkg/proto/timeutil/time.go | 40 + api/pkg/server/factory.go | 5 + api/pkg/server/grpcapp/app.go | 273 ++++ api/pkg/server/grpcapp/config.go | 49 + api/pkg/server/internal/instance.go | 40 + api/pkg/server/internal/server.go | 58 + api/pkg/server/main/run.go | 11 + api/pkg/server/server.go | 6 + api/pkg/tagdb.test | Bin 0 -> 25998146 bytes api/pkg/version/factory/factory.go | 10 + api/pkg/version/info.go | 9 + api/pkg/version/internal/version.go | 106 ++ api/pkg/version/version.go | 10 + api/proto/account_created.proto | 7 + api/proto/billing/fees/v1/fees.proto | 161 ++ api/proto/chain/gateway/v1/gateway.proto | 216 +++ api/proto/common/accounting/v1/posting.proto | 19 + api/proto/common/fx/v1/fx.proto | 14 + api/proto/common/money/v1/money.proto | 23 + api/proto/common/pagination/v1/cursor.proto | 12 + api/proto/common/trace/v1/trace.proto | 9 + api/proto/envelope.proto | 22 + api/proto/ledger/v1/ledger.proto | 194 +++ api/proto/notification_sent.proto | 13 + api/proto/object_updated.proto | 8 + api/proto/operation_result.proto | 8 + api/proto/oracle/v1/oracle.proto | 125 ++ api/proto/password_reset.proto | 8 + .../orchestrator/v1/orchestrator.proto | 222 +++ 537 files changed, 48453 insertions(+) create mode 100644 .gitignore create mode 100644 api/billing/fees/.air.toml create mode 100644 api/billing/fees/.gitignore create mode 100644 api/billing/fees/config.yml create mode 100644 api/billing/fees/env/.gitignore create mode 100644 api/billing/fees/go.mod create mode 100644 api/billing/fees/go.sum create mode 100644 api/billing/fees/internal/appversion/version.go create mode 100644 api/billing/fees/internal/server/internal/serverimp.go create mode 100644 api/billing/fees/internal/server/server.go create mode 100644 api/billing/fees/internal/service/fees/calculator.go create mode 100644 api/billing/fees/internal/service/fees/metrics.go create mode 100644 api/billing/fees/internal/service/fees/options.go create mode 100644 api/billing/fees/internal/service/fees/service.go create mode 100644 api/billing/fees/internal/service/fees/service_test.go create mode 100644 api/billing/fees/main.go create mode 100644 api/billing/fees/storage/model/plan.go create mode 100644 api/billing/fees/storage/mongo/repository.go create mode 100644 api/billing/fees/storage/mongo/store/plans.go create mode 100644 api/billing/fees/storage/storage.go create mode 100644 api/chain/gateway/.air.toml create mode 100644 api/chain/gateway/.gitignore create mode 100644 api/chain/gateway/client/client.go create mode 100644 api/chain/gateway/client/config.go create mode 100644 api/chain/gateway/client/fake.go create mode 100644 api/chain/gateway/config.yml create mode 100644 api/chain/gateway/go.mod create mode 100644 api/chain/gateway/go.sum create mode 100644 api/chain/gateway/internal/appversion/version.go create mode 100644 api/chain/gateway/internal/keymanager/config.go create mode 100644 api/chain/gateway/internal/keymanager/keymanager.go create mode 100644 api/chain/gateway/internal/keymanager/vault/manager.go create mode 100644 api/chain/gateway/internal/server/internal/serverimp.go create mode 100644 api/chain/gateway/internal/server/server.go create mode 100644 api/chain/gateway/internal/service/gateway/conversion_helpers.go create mode 100644 api/chain/gateway/internal/service/gateway/executor.go create mode 100644 api/chain/gateway/internal/service/gateway/metrics.go create mode 100644 api/chain/gateway/internal/service/gateway/options.go create mode 100644 api/chain/gateway/internal/service/gateway/service.go create mode 100644 api/chain/gateway/internal/service/gateway/service_test.go create mode 100644 api/chain/gateway/internal/service/gateway/transfer_execution.go create mode 100644 api/chain/gateway/internal/service/gateway/transfer_handlers.go create mode 100644 api/chain/gateway/internal/service/gateway/wallet_handlers.go create mode 100644 api/chain/gateway/main.go create mode 100644 api/chain/gateway/storage/model/deposit.go create mode 100644 api/chain/gateway/storage/model/transfer.go create mode 100644 api/chain/gateway/storage/model/wallet.go create mode 100644 api/chain/gateway/storage/mongo/repository.go create mode 100644 api/chain/gateway/storage/mongo/store/deposits.go create mode 100644 api/chain/gateway/storage/mongo/store/transfers.go create mode 100644 api/chain/gateway/storage/mongo/store/wallets.go create mode 100644 api/chain/gateway/storage/storage.go create mode 100644 api/fx/ingestor/.DS_Store create mode 100644 api/fx/ingestor/.air.toml create mode 100644 api/fx/ingestor/.gitignore create mode 100644 api/fx/ingestor/config.yml create mode 100644 api/fx/ingestor/env/.gitignore create mode 100644 api/fx/ingestor/go.mod create mode 100644 api/fx/ingestor/go.sum create mode 100644 api/fx/ingestor/internal/appversion/version.go create mode 100644 api/fx/ingestor/internal/config/config.go create mode 100644 api/fx/ingestor/internal/config/market.go create mode 100644 api/fx/ingestor/internal/config/metrics.go create mode 100644 api/fx/ingestor/internal/fmerrors/market.go create mode 100644 api/fx/ingestor/internal/ingestor/metrics.go create mode 100644 api/fx/ingestor/internal/ingestor/service.go create mode 100644 api/fx/ingestor/internal/ingestor/service_test.go create mode 100644 api/fx/ingestor/internal/market/binance/connector.go create mode 100644 api/fx/ingestor/internal/market/coingecko/connector.go create mode 100644 api/fx/ingestor/internal/market/common/settings.go create mode 100644 api/fx/ingestor/internal/market/factory.go create mode 100644 api/fx/ingestor/internal/metrics/server.go create mode 100644 api/fx/ingestor/internal/model/connector.go create mode 100644 api/fx/ingestor/internal/model/ticker.go create mode 100644 api/fx/ingestor/internal/signalctx/signalctx.go create mode 100644 api/fx/ingestor/main.go create mode 100644 api/fx/oracle/.air.toml create mode 100644 api/fx/oracle/.gitignore create mode 100644 api/fx/oracle/client/client.go create mode 100644 api/fx/oracle/client/client_test.go create mode 100644 api/fx/oracle/client/config.go create mode 100644 api/fx/oracle/client/fake.go create mode 100644 api/fx/oracle/config.yml create mode 100644 api/fx/oracle/env/.gitignore create mode 100644 api/fx/oracle/go.mod create mode 100644 api/fx/oracle/go.sum create mode 100644 api/fx/oracle/internal/appversion/version.go create mode 100644 api/fx/oracle/internal/server/internal/serverimp.go create mode 100644 api/fx/oracle/internal/server/server.go create mode 100644 api/fx/oracle/internal/service/oracle/calculator.go create mode 100644 api/fx/oracle/internal/service/oracle/cross.go create mode 100644 api/fx/oracle/internal/service/oracle/math.go create mode 100644 api/fx/oracle/internal/service/oracle/metrics.go create mode 100644 api/fx/oracle/internal/service/oracle/service.go create mode 100644 api/fx/oracle/internal/service/oracle/service_test.go create mode 100644 api/fx/oracle/internal/service/oracle/transform.go create mode 100644 api/fx/oracle/main.go create mode 100644 api/fx/storage/.gitignore create mode 100644 api/fx/storage/go.mod create mode 100644 api/fx/storage/go.sum create mode 100644 api/fx/storage/model/cross.go create mode 100644 api/fx/storage/model/currency.go create mode 100644 api/fx/storage/model/pair.go create mode 100644 api/fx/storage/model/quote.go create mode 100644 api/fx/storage/model/rate.go create mode 100644 api/fx/storage/model/types.go create mode 100644 api/fx/storage/mongo/repository.go create mode 100644 api/fx/storage/mongo/store/currency.go create mode 100644 api/fx/storage/mongo/store/currency_test.go create mode 100644 api/fx/storage/mongo/store/pair.go create mode 100644 api/fx/storage/mongo/store/pair_test.go create mode 100644 api/fx/storage/mongo/store/quotes.go create mode 100644 api/fx/storage/mongo/store/quotes_test.go create mode 100644 api/fx/storage/mongo/store/rates.go create mode 100644 api/fx/storage/mongo/store/rates_test.go create mode 100644 api/fx/storage/mongo/store/testing_helpers_test.go create mode 100644 api/fx/storage/mongo/transaction.go create mode 100644 api/fx/storage/storage.go create mode 100644 api/ledger/.air.toml create mode 100644 api/ledger/.gitignore create mode 100644 api/ledger/METRICS.md create mode 100644 api/ledger/client/client.go create mode 100644 api/ledger/client/config.go create mode 100644 api/ledger/client/fake.go create mode 100644 api/ledger/config.yml create mode 100644 api/ledger/env/.gitignore create mode 100644 api/ledger/go.mod create mode 100644 api/ledger/go.sum create mode 100644 api/ledger/internal/appversion/version.go create mode 100644 api/ledger/internal/model/account.go create mode 100644 api/ledger/internal/model/balance.go create mode 100644 api/ledger/internal/model/jentry.go create mode 100644 api/ledger/internal/model/outbox.go create mode 100644 api/ledger/internal/model/ownership.go create mode 100644 api/ledger/internal/model/party.go create mode 100644 api/ledger/internal/model/pline.go create mode 100644 api/ledger/internal/model/util.go create mode 100644 api/ledger/internal/model/validation.go create mode 100644 api/ledger/internal/server/internal/serverimp.go create mode 100644 api/ledger/internal/server/server.go create mode 100644 api/ledger/internal/service/ledger/accounts.go create mode 100644 api/ledger/internal/service/ledger/accounts_test.go create mode 100644 api/ledger/internal/service/ledger/helpers.go create mode 100644 api/ledger/internal/service/ledger/helpers_test.go create mode 100644 api/ledger/internal/service/ledger/metrics.go create mode 100644 api/ledger/internal/service/ledger/outbox_publisher.go create mode 100644 api/ledger/internal/service/ledger/outbox_publisher_test.go create mode 100644 api/ledger/internal/service/ledger/posting.go create mode 100644 api/ledger/internal/service/ledger/posting_debit.go create mode 100644 api/ledger/internal/service/ledger/posting_fx.go create mode 100644 api/ledger/internal/service/ledger/posting_support.go create mode 100644 api/ledger/internal/service/ledger/posting_support_test.go create mode 100644 api/ledger/internal/service/ledger/posting_transfer.go create mode 100644 api/ledger/internal/service/ledger/queries.go create mode 100644 api/ledger/internal/service/ledger/queries_test.go create mode 100644 api/ledger/internal/service/ledger/service.go create mode 100644 api/ledger/main.go create mode 100644 api/ledger/storage/model/account.go create mode 100644 api/ledger/storage/model/account_balance.go create mode 100644 api/ledger/storage/model/journal_entry.go create mode 100644 api/ledger/storage/model/outbox.go create mode 100644 api/ledger/storage/model/posting_line.go create mode 100644 api/ledger/storage/model/types.go create mode 100644 api/ledger/storage/mongo/repository.go create mode 100644 api/ledger/storage/mongo/store/accounts.go create mode 100644 api/ledger/storage/mongo/store/accounts_test.go create mode 100644 api/ledger/storage/mongo/store/balances.go create mode 100644 api/ledger/storage/mongo/store/balances_test.go create mode 100644 api/ledger/storage/mongo/store/journal_entries.go create mode 100644 api/ledger/storage/mongo/store/journal_entries_test.go create mode 100644 api/ledger/storage/mongo/store/outbox.go create mode 100644 api/ledger/storage/mongo/store/outbox_test.go create mode 100644 api/ledger/storage/mongo/store/posting_lines.go create mode 100644 api/ledger/storage/mongo/store/posting_lines_test.go create mode 100644 api/ledger/storage/mongo/store/testing_helpers_test.go create mode 100644 api/ledger/storage/mongo/transaction.go create mode 100644 api/ledger/storage/repository.go create mode 100644 api/ledger/storage/storage.go create mode 100644 api/payments/orchestrator/.gitignore create mode 100644 api/payments/orchestrator/client/client.go create mode 100644 api/payments/orchestrator/client/config.go create mode 100644 api/payments/orchestrator/client/fake.go create mode 100644 api/payments/orchestrator/env/.gitignore create mode 100644 api/payments/orchestrator/go.mod create mode 100644 api/payments/orchestrator/go.sum create mode 100644 api/payments/orchestrator/internal/service/orchestrator/convert.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/execution.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/helpers.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/internal_helpers.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/metrics.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/options.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/service.go create mode 100644 api/payments/orchestrator/internal/service/orchestrator/service_test.go create mode 100644 api/payments/orchestrator/storage/model/payment.go create mode 100644 api/payments/orchestrator/storage/mongo/repository.go create mode 100644 api/payments/orchestrator/storage/mongo/store/payments.go create mode 100644 api/payments/orchestrator/storage/storage.go create mode 100644 api/pkg/.DS_Store create mode 100644 api/pkg/.gitignore create mode 100644 api/pkg/api/http/methods.go create mode 100644 api/pkg/api/http/response/response.go create mode 100644 api/pkg/api/http/response/result.go create mode 100644 api/pkg/api/http/status.go create mode 100644 api/pkg/api/routers/grpc.go create mode 100644 api/pkg/api/routers/gsresponse/response.go create mode 100644 api/pkg/api/routers/gsresponse/response_test.go create mode 100644 api/pkg/api/routers/health.go create mode 100644 api/pkg/api/routers/health/status.go create mode 100644 api/pkg/api/routers/internal/grpcimp/config.go create mode 100644 api/pkg/api/routers/internal/grpcimp/metrics.go create mode 100644 api/pkg/api/routers/internal/grpcimp/options.go create mode 100644 api/pkg/api/routers/internal/grpcimp/router.go create mode 100644 api/pkg/api/routers/internal/grpcimp/router_test.go create mode 100644 api/pkg/api/routers/internal/healthimp/health.go create mode 100644 api/pkg/api/routers/internal/healthimp/status.go create mode 100644 api/pkg/api/routers/internal/messagingimp/consumer.go create mode 100644 api/pkg/api/routers/internal/messagingimp/messsaging.go create mode 100644 api/pkg/api/routers/messaging.go create mode 100644 api/pkg/auth/USAGE.md create mode 100644 api/pkg/auth/anyobject/anyobject.go create mode 100644 api/pkg/auth/archivable.go create mode 100644 api/pkg/auth/archivableimp.go create mode 100644 api/pkg/auth/config.go create mode 100644 api/pkg/auth/customizable/customizable.go create mode 100644 api/pkg/auth/customizable/manager.go create mode 100644 api/pkg/auth/db.go create mode 100644 api/pkg/auth/dbab.go create mode 100644 api/pkg/auth/dbimp.go create mode 100644 api/pkg/auth/dbimpab.go create mode 100644 api/pkg/auth/dbimpab_test.go create mode 100644 api/pkg/auth/enforcer.go create mode 100644 api/pkg/auth/factory.go create mode 100644 api/pkg/auth/helper.go create mode 100644 api/pkg/auth/indexable.go create mode 100644 api/pkg/auth/indexableimp.go create mode 100644 api/pkg/auth/internal/casbin/action.go create mode 100644 api/pkg/auth/internal/casbin/config/config.go create mode 100644 api/pkg/auth/internal/casbin/enforcer.go create mode 100644 api/pkg/auth/internal/casbin/factory.go create mode 100644 api/pkg/auth/internal/casbin/logger.go create mode 100644 api/pkg/auth/internal/casbin/manager.go create mode 100644 api/pkg/auth/internal/casbin/models/auth.conf create mode 100644 api/pkg/auth/internal/casbin/permissions.go create mode 100644 api/pkg/auth/internal/casbin/role.go create mode 100644 api/pkg/auth/internal/casbin/serialization/internal/policy.go create mode 100644 api/pkg/auth/internal/casbin/serialization/internal/role.go create mode 100644 api/pkg/auth/internal/casbin/serialization/policy.go create mode 100644 api/pkg/auth/internal/casbin/serialization/role.go create mode 100644 api/pkg/auth/internal/casbin/serialization/serializer.go create mode 100644 api/pkg/auth/internal/native/db/policies.go create mode 100644 api/pkg/auth/internal/native/db/roles.go create mode 100644 api/pkg/auth/internal/native/dbpolicies.go create mode 100644 api/pkg/auth/internal/native/dbroles.go create mode 100644 api/pkg/auth/internal/native/enforcer.go create mode 100644 api/pkg/auth/internal/native/enforcer_test.go create mode 100644 api/pkg/auth/internal/native/manager.go create mode 100755 api/pkg/auth/internal/native/native.test create mode 100644 api/pkg/auth/internal/native/nstructures/policies.go create mode 100644 api/pkg/auth/internal/native/nstructures/role.go create mode 100644 api/pkg/auth/internal/native/permission.go create mode 100644 api/pkg/auth/internal/native/role.go create mode 100644 api/pkg/auth/management/permission.go create mode 100644 api/pkg/auth/management/role.go create mode 100644 api/pkg/auth/manager.go create mode 100644 api/pkg/auth/provider.go create mode 100644 api/pkg/auth/taggable.go create mode 100644 api/pkg/auth/taggableimp.go create mode 100644 api/pkg/clock/clock.go create mode 100755 api/pkg/db/account/account.go create mode 100644 api/pkg/db/config.go create mode 100644 api/pkg/db/connection.go create mode 100644 api/pkg/db/factory.go create mode 100644 api/pkg/db/indexable/indexable.go create mode 100644 api/pkg/db/internal/mongo/accountdb/db.go create mode 100644 api/pkg/db/internal/mongo/accountdb/token.go create mode 100755 api/pkg/db/internal/mongo/accountdb/user.go create mode 100644 api/pkg/db/internal/mongo/archivable/archivable.go create mode 100644 api/pkg/db/internal/mongo/archivable/archivable_test.go create mode 100755 api/pkg/db/internal/mongo/db.go create mode 100644 api/pkg/db/internal/mongo/indexable/README.md create mode 100644 api/pkg/db/internal/mongo/indexable/USAGE.md create mode 100644 api/pkg/db/internal/mongo/indexable/examples.go create mode 100644 api/pkg/db/internal/mongo/indexable/indexable.go create mode 100644 api/pkg/db/internal/mongo/indexable/indexable_test.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/accept.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/archived.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/cascade.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/db.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/decline.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/getpublic.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/list.go create mode 100644 api/pkg/db/internal/mongo/invitationdb/updatestatus.go create mode 100644 api/pkg/db/internal/mongo/mongo.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/archived.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/cascade.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/create.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/db.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/get.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/list.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/owned.go create mode 100644 api/pkg/db/internal/mongo/organizationdb/setarchived_test.go create mode 100644 api/pkg/db/internal/mongo/policiesdb/all.go create mode 100644 api/pkg/db/internal/mongo/policiesdb/builtin.go create mode 100644 api/pkg/db/internal/mongo/policiesdb/db.go create mode 100644 api/pkg/db/internal/mongo/policiesdb/db_test.go create mode 100644 api/pkg/db/internal/mongo/policiesdb/policies.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/client.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/crud.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/db.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/fields.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/filters.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/refreshtokensdb_test.go create mode 100644 api/pkg/db/internal/mongo/refreshtokensdb/revoke.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/accumulator.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/alias.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/array.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/expression.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/field.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/func.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/gaccumulator.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/patch.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/pipeline_test.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/projection.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/query.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/builderimp/value.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/index.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/repository.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/repository_comprehensive_test.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/repository_insertmany_test.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/repository_patch_test.go create mode 100644 api/pkg/db/internal/mongo/repositoryimp/repository_test.go create mode 100644 api/pkg/db/internal/mongo/rolesdb/db.go create mode 100644 api/pkg/db/internal/mongo/rolesdb/list.go create mode 100644 api/pkg/db/internal/mongo/rolesdb/roles.go create mode 100644 api/pkg/db/internal/mongo/transactionimp/factory.go create mode 100644 api/pkg/db/internal/mongo/transactionimp/transaction.go create mode 100644 api/pkg/db/internal/mongo/tseriesimp/tseries.go create mode 100644 api/pkg/db/invitation/invitation.go create mode 100644 api/pkg/db/organization/organization.go create mode 100644 api/pkg/db/policy/policy.go create mode 100644 api/pkg/db/refreshtokens/refreshtokens.go create mode 100644 api/pkg/db/repository/abfilter.go create mode 100644 api/pkg/db/repository/builder/accumulator.go create mode 100644 api/pkg/db/repository/builder/alias.go create mode 100644 api/pkg/db/repository/builder/array.go create mode 100644 api/pkg/db/repository/builder/expression.go create mode 100644 api/pkg/db/repository/builder/field.go create mode 100644 api/pkg/db/repository/builder/keyword.go create mode 100644 api/pkg/db/repository/builder/operators.go create mode 100644 api/pkg/db/repository/builder/patch.go create mode 100644 api/pkg/db/repository/builder/pipeline.go create mode 100644 api/pkg/db/repository/builder/projection.go create mode 100644 api/pkg/db/repository/builder/query.go create mode 100644 api/pkg/db/repository/builder/unwind.go create mode 100644 api/pkg/db/repository/builder/value.go create mode 100644 api/pkg/db/repository/builders.go create mode 100644 api/pkg/db/repository/cursor.go create mode 100644 api/pkg/db/repository/decoder/decoder.go create mode 100644 api/pkg/db/repository/filter_factory_test.go create mode 100644 api/pkg/db/repository/index/index.go create mode 100644 api/pkg/db/repository/index/types.go create mode 100644 api/pkg/db/repository/repository.go create mode 100644 api/pkg/db/role/role.go create mode 100644 api/pkg/db/storable/id.go create mode 100644 api/pkg/db/storable/ref.go create mode 100644 api/pkg/db/storable/storable.go create mode 100644 api/pkg/db/tag/tag.go create mode 100644 api/pkg/db/template/interface.go create mode 100644 api/pkg/db/template/template.go create mode 100644 api/pkg/db/template/tseries.go create mode 100644 api/pkg/db/transaction/factory.go create mode 100644 api/pkg/db/transaction/transaction.go create mode 100644 api/pkg/db/tseries/factory.go create mode 100644 api/pkg/db/tseries/options/options.go create mode 100644 api/pkg/db/tseries/point/interface.go create mode 100644 api/pkg/db/tseries/template.go create mode 100644 api/pkg/db/tseries/tseries.go create mode 100644 api/pkg/decimal/money.go create mode 100644 api/pkg/decimal/rational.go create mode 100644 api/pkg/decimal/rounding.go create mode 100644 api/pkg/domainprovider/domain_provider.go create mode 100644 api/pkg/domainprovider/imp/domain_provider.go create mode 100644 api/pkg/go.mod create mode 100644 api/pkg/go.sum create mode 100644 api/pkg/localization/locale.go create mode 100644 api/pkg/localization/localization.go create mode 100644 api/pkg/merrors/errors.go create mode 100644 api/pkg/messaging/broker/broker.go create mode 100644 api/pkg/messaging/config.go create mode 100644 api/pkg/messaging/consumer.go create mode 100644 api/pkg/messaging/envelope/envelope.go create mode 100644 api/pkg/messaging/factory.go create mode 100644 api/pkg/messaging/handler.go create mode 100644 api/pkg/messaging/inprocess/inprocess.go create mode 100644 api/pkg/messaging/internal/.gitignore create mode 100644 api/pkg/messaging/internal/envelope/envelope.go create mode 100644 api/pkg/messaging/internal/inprocess/broker.go create mode 100644 api/pkg/messaging/internal/inprocess/config/config.go create mode 100644 api/pkg/messaging/internal/natsb/NATS.go create mode 100644 api/pkg/messaging/internal/natsb/broker.go create mode 100644 api/pkg/messaging/internal/natsb/config/config.go create mode 100644 api/pkg/messaging/internal/natsb/subscription.go create mode 100644 api/pkg/messaging/internal/notifications/account/notification.go create mode 100644 api/pkg/messaging/internal/notifications/account/password_reset.go create mode 100644 api/pkg/messaging/internal/notifications/account/password_reset_processor.go create mode 100644 api/pkg/messaging/internal/notifications/account/processor.go create mode 100644 api/pkg/messaging/internal/notifications/invitation/processor.go create mode 100644 api/pkg/messaging/internal/notifications/notification/notification.go create mode 100644 api/pkg/messaging/internal/notifications/notification/processor.go create mode 100644 api/pkg/messaging/internal/notifications/object/object.go create mode 100644 api/pkg/messaging/internal/notifications/object/processor.go create mode 100644 api/pkg/messaging/internal/producer/producer.go create mode 100644 api/pkg/messaging/message/message.go create mode 100644 api/pkg/messaging/messaging.go create mode 100644 api/pkg/messaging/natsb/nats.go create mode 100644 api/pkg/messaging/notifications/account/created.go create mode 100644 api/pkg/messaging/notifications/account/handler/interface.go create mode 100644 api/pkg/messaging/notifications/account/password_reset.go create mode 100644 api/pkg/messaging/notifications/account/processor.go create mode 100644 api/pkg/messaging/notifications/invitation/handler/interface.go create mode 100644 api/pkg/messaging/notifications/invitation/invitation.go create mode 100644 api/pkg/messaging/notifications/invitation/processor.go create mode 100644 api/pkg/messaging/notifications/notification/handler/interface.go create mode 100644 api/pkg/messaging/notifications/notification/sent.go create mode 100644 api/pkg/messaging/notifications/object/handler/interface.go create mode 100644 api/pkg/messaging/notifications/object/object.go create mode 100644 api/pkg/messaging/notifications/object/processor.go create mode 100644 api/pkg/messaging/notifications/processor/envelope.go create mode 100644 api/pkg/messaging/producer.go create mode 100644 api/pkg/messaging/producer/producer.go create mode 100644 api/pkg/mlogger/factory/mlogger.go create mode 100644 api/pkg/mlogger/internal/mlogger/mlogger.go create mode 100644 api/pkg/mlogger/logger.go create mode 100755 api/pkg/model/account.go create mode 100644 api/pkg/model/ampli.go create mode 100644 api/pkg/model/archivable.go create mode 100644 api/pkg/model/attachment.go create mode 100644 api/pkg/model/auth.go create mode 100644 api/pkg/model/automation.go create mode 100644 api/pkg/model/client.go create mode 100644 api/pkg/model/colorable.go create mode 100644 api/pkg/model/comment.go create mode 100644 api/pkg/model/commentp.go create mode 100644 api/pkg/model/currency.go create mode 100644 api/pkg/model/customizable.go create mode 100644 api/pkg/model/describable.go create mode 100644 api/pkg/model/dzone.go create mode 100644 api/pkg/model/fconfig.go create mode 100644 api/pkg/model/filter.go create mode 100644 api/pkg/model/indexable.go create mode 100644 api/pkg/model/internal/notificationevent.go create mode 100644 api/pkg/model/invitation.go create mode 100644 api/pkg/model/invoice.go create mode 100644 api/pkg/model/link.go create mode 100644 api/pkg/model/notification/notification.go create mode 100644 api/pkg/model/notificationevent.go create mode 100644 api/pkg/model/nresult.go create mode 100644 api/pkg/model/object.go create mode 100644 api/pkg/model/opresult.go create mode 100644 api/pkg/model/organization.go create mode 100644 api/pkg/model/pbinding.go create mode 100644 api/pkg/model/permission.go create mode 100644 api/pkg/model/pfilter.go create mode 100644 api/pkg/model/priority.go create mode 100644 api/pkg/model/project.go create mode 100644 api/pkg/model/property.go create mode 100644 api/pkg/model/reaction.go create mode 100644 api/pkg/model/refresh.go create mode 100644 api/pkg/model/sessionid.go create mode 100644 api/pkg/model/status.go create mode 100644 api/pkg/model/step.go create mode 100644 api/pkg/model/tag.go create mode 100644 api/pkg/model/task.go create mode 100644 api/pkg/model/team.go create mode 100644 api/pkg/model/tenant.go create mode 100644 api/pkg/model/userdata.go create mode 100644 api/pkg/model/value.go create mode 100644 api/pkg/model/value_test.go create mode 100644 api/pkg/model/viewcursor.go create mode 100644 api/pkg/model/workflow.go create mode 100644 api/pkg/model/workspace.go create mode 100644 api/pkg/mservice/mservice.go create mode 100644 api/pkg/mservice/services.go create mode 100644 api/pkg/mutil/config/param.go create mode 100644 api/pkg/mutil/db/archive.go create mode 100644 api/pkg/mutil/db/array.go create mode 100644 api/pkg/mutil/db/auth/accountbound.go create mode 100644 api/pkg/mutil/db/auth/protected.go create mode 100644 api/pkg/mutil/db/db.go create mode 100644 api/pkg/mutil/duration/duration.go create mode 100644 api/pkg/mutil/fr/fr.go create mode 100644 api/pkg/mutil/helpers/accountmanager.go create mode 100644 api/pkg/mutil/helpers/factory.go create mode 100644 api/pkg/mutil/helpers/integration_test.go create mode 100644 api/pkg/mutil/helpers/internal/accountmanager.go create mode 100644 api/pkg/mutil/helpers/internal/simple_internal_test.go create mode 100644 api/pkg/mutil/helpers/internal/task_manager_business_test.go create mode 100644 api/pkg/mutil/helpers/internal/taskmanager.go create mode 100644 api/pkg/mutil/helpers/simple_test.go create mode 100644 api/pkg/mutil/helpers/taskmanager.go create mode 100644 api/pkg/mutil/helpers/taskmanager_factory.go create mode 100644 api/pkg/mutil/http/http.go create mode 100644 api/pkg/mutil/imagewriter/imagewriter.go create mode 100644 api/pkg/mutil/mzap/envelope.go create mode 100644 api/pkg/mutil/mzap/object.go create mode 100644 api/pkg/mutil/reorder/reorder.go create mode 100644 api/pkg/mutil/time/go/gotime.go create mode 100644 api/pkg/proto/timeutil/time.go create mode 100644 api/pkg/server/factory.go create mode 100644 api/pkg/server/grpcapp/app.go create mode 100644 api/pkg/server/grpcapp/config.go create mode 100644 api/pkg/server/internal/instance.go create mode 100644 api/pkg/server/internal/server.go create mode 100644 api/pkg/server/main/run.go create mode 100644 api/pkg/server/server.go create mode 100755 api/pkg/tagdb.test create mode 100644 api/pkg/version/factory/factory.go create mode 100644 api/pkg/version/info.go create mode 100644 api/pkg/version/internal/version.go create mode 100644 api/pkg/version/version.go create mode 100644 api/proto/account_created.proto create mode 100644 api/proto/billing/fees/v1/fees.proto create mode 100644 api/proto/chain/gateway/v1/gateway.proto create mode 100644 api/proto/common/accounting/v1/posting.proto create mode 100644 api/proto/common/fx/v1/fx.proto create mode 100644 api/proto/common/money/v1/money.proto create mode 100644 api/proto/common/pagination/v1/cursor.proto create mode 100644 api/proto/common/trace/v1/trace.proto create mode 100644 api/proto/envelope.proto create mode 100644 api/proto/ledger/v1/ledger.proto create mode 100644 api/proto/notification_sent.proto create mode 100644 api/proto/object_updated.proto create mode 100644 api/proto/operation_result.proto create mode 100644 api/proto/oracle/v1/oracle.proto create mode 100644 api/proto/password_reset.proto create mode 100644 api/proto/payments/orchestrator/v1/orchestrator.proto diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..86ea028 --- /dev/null +++ b/.gitignore @@ -0,0 +1 @@ +.env.version \ No newline at end of file diff --git a/api/billing/fees/.air.toml b/api/billing/fees/.air.toml new file mode 100644 index 0000000..e1c0568 --- /dev/null +++ b/api/billing/fees/.air.toml @@ -0,0 +1,32 @@ +# Config file for Air in TOML format + +root = "./../.." +tmp_dir = "tmp" + +[build] +cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/billing/fees/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/billing/fees/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/billing/fees/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/billing/fees/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/billing/fees/internal/appversion.BuildDate=$(date)'\"" +bin = "./app" +full_bin = "./app --debug --config.file=config.yml" +include_ext = ["go", "yaml", "yml"] +exclude_dir = ["billing/fees/tmp", "pkg/.git", "billing/fees/env"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true +follow_symlink = true +log = "air.log" +delay = 0 +stop_on_error = true +send_interrupt = true +kill_delay = 500 +args_bin = [] + +[log] +time = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true diff --git a/api/billing/fees/.gitignore b/api/billing/fees/.gitignore new file mode 100644 index 0000000..c62beb6 --- /dev/null +++ b/api/billing/fees/.gitignore @@ -0,0 +1,3 @@ +internal/generated +.gocache +app diff --git a/api/billing/fees/config.yml b/api/billing/fees/config.yml new file mode 100644 index 0000000..615919e --- /dev/null +++ b/api/billing/fees/config.yml @@ -0,0 +1,40 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50060" + enable_reflection: true + enable_health: true + +metrics: + address: ":9402" + +database: + driver: mongodb + settings: + host_env: FEES_MONGO_HOST + port_env: FEES_MONGO_PORT + database_env: FEES_MONGO_DATABASE + user_env: FEES_MONGO_USER + password_env: FEES_MONGO_PASSWORD + auth_source_env: FEES_MONGO_AUTH_SOURCE + replica_set_env: FEES_MONGO_REPLICA_SET + +messaging: + driver: NATS + settings: + url_env: NATS_URL + host_env: NATS_HOST + port_env: NATS_PORT + username_env: NATS_USER + password_env: NATS_PASSWORD + broker_name: Billing Fees Service + max_reconnects: 10 + reconnect_wait: 5 + +oracle: + address: "sendico_fx_oracle:50051" + dial_timeout_seconds: 5 + call_timeout_seconds: 3 + insecure: true diff --git a/api/billing/fees/env/.gitignore b/api/billing/fees/env/.gitignore new file mode 100644 index 0000000..f2a8cbe --- /dev/null +++ b/api/billing/fees/env/.gitignore @@ -0,0 +1 @@ +.env.api diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod new file mode 100644 index 0000000..8706f31 --- /dev/null +++ b/api/billing/fees/go.mod @@ -0,0 +1,54 @@ +module github.com/tech/sendico/billing/fees + +go 1.25.3 + +replace github.com/tech/sendico/pkg => ../../pkg + +replace github.com/tech/sendico/fx/oracle => ../../fx/oracle + +require ( + github.com/tech/sendico/fx/oracle v0.0.0 + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver v1.17.6 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.76.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/prometheus/client_golang v1.23.2 + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/protobuf v1.36.10 +) diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum new file mode 100644 index 0000000..1558ea2 --- /dev/null +++ b/api/billing/fees/go.sum @@ -0,0 +1,225 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= +github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +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/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +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/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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/billing/fees/internal/appversion/version.go b/api/billing/fees/internal/appversion/version.go new file mode 100644 index 0000000..de65b6a --- /dev/null +++ b/api/billing/fees/internal/appversion/version.go @@ -0,0 +1,28 @@ +package appversion + +import ( + "github.com/tech/sendico/pkg/version" + vf "github.com/tech/sendico/pkg/version/factory" +) + +// Build information populated at build time. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +// Create initialises a version.Printer with the build details for this service. +func Create() version.Printer { + info := version.Info{ + Program: "Sendico Billing Fees Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&info) +} diff --git a/api/billing/fees/internal/server/internal/serverimp.go b/api/billing/fees/internal/server/internal/serverimp.go new file mode 100644 index 0000000..01f85cb --- /dev/null +++ b/api/billing/fees/internal/server/internal/serverimp.go @@ -0,0 +1,163 @@ +package serverimp + +import ( + "context" + "os" + "strings" + "time" + + "github.com/tech/sendico/billing/fees/internal/service/fees" + "github.com/tech/sendico/billing/fees/storage" + mongostorage "github.com/tech/sendico/billing/fees/storage/mongo" + oracleclient "github.com/tech/sendico/fx/oracle/client" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/db" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type Imp struct { + logger mlogger.Logger + file string + debug bool + config *config + app *grpcapp.App[storage.Repository] + oracleClient oracleclient.Client +} + +type config struct { + *grpcapp.Config `yaml:",inline"` + Oracle OracleConfig `yaml:"oracle"` +} + +type OracleConfig struct { + Address string `yaml:"address"` + DialTimeoutSecs int `yaml:"dial_timeout_seconds"` + CallTimeoutSecs int `yaml:"call_timeout_seconds"` + InsecureTransport bool `yaml:"insecure"` +} + +func (c OracleConfig) dialTimeout() time.Duration { + if c.DialTimeoutSecs <= 0 { + return 5 * time.Second + } + return time.Duration(c.DialTimeoutSecs) * time.Second +} + +func (c OracleConfig) callTimeout() time.Duration { + if c.CallTimeoutSecs <= 0 { + return 3 * time.Second + } + return time.Duration(c.CallTimeoutSecs) * time.Second +} + +// Create initialises the billing fees server implementation. +func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { + return &Imp{ + logger: logger.Named("server"), + file: file, + debug: debug, + }, nil +} + +func (i *Imp) Shutdown() { + if i.app == nil { + if i.oracleClient != nil { + _ = i.oracleClient.Close() + } + return + } + + timeout := 15 * time.Second + if i.config != nil && i.config.Runtime != nil { + timeout = i.config.Runtime.ShutdownTimeout() + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + i.app.Shutdown(ctx) + cancel() + + if i.oracleClient != nil { + _ = i.oracleClient.Close() + } +} + +func (i *Imp) Start() error { + cfg, err := i.loadConfig() + if err != nil { + return err + } + i.config = cfg + + repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { + return mongostorage.New(logger, conn) + } + + var oracleClient oracleclient.Client + if addr := strings.TrimSpace(cfg.Oracle.Address); addr != "" { + dialCtx, cancel := context.WithTimeout(context.Background(), cfg.Oracle.dialTimeout()) + defer cancel() + + oc, err := oracleclient.New(dialCtx, oracleclient.Config{ + Address: addr, + DialTimeout: cfg.Oracle.dialTimeout(), + CallTimeout: cfg.Oracle.callTimeout(), + Insecure: cfg.Oracle.InsecureTransport, + }) + if err != nil { + i.logger.Warn("failed to initialise oracle client", zap.String("address", addr), zap.Error(err)) + } else { + oracleClient = oc + i.oracleClient = oc + i.logger.Info("connected to oracle service", zap.String("address", addr)) + } + } + + serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { + opts := []fees.Option{} + if oracleClient != nil { + opts = append(opts, fees.WithOracleClient(oracleClient)) + } + return fees.NewService(logger, repo, producer, opts...), nil + } + + app, err := grpcapp.NewApp(i.logger, "billing_fees", cfg.Config, i.debug, repoFactory, serviceFactory) + if err != nil { + return err + } + i.app = app + + return i.app.Start() +} + +func (i *Imp) loadConfig() (*config, error) { + data, err := os.ReadFile(i.file) + if err != nil { + i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) + return nil, err + } + + cfg := &config{Config: &grpcapp.Config{}} + if err := yaml.Unmarshal(data, cfg); err != nil { + i.logger.Error("Failed to parse configuration", zap.Error(err)) + return nil, err + } + + if cfg.Runtime == nil { + cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} + } + + if cfg.GRPC == nil { + cfg.GRPC = &routers.GRPCConfig{ + Network: "tcp", + Address: ":50060", + EnableReflection: true, + EnableHealth: true, + } + } + + return cfg, nil +} diff --git a/api/billing/fees/internal/server/server.go b/api/billing/fees/internal/server/server.go new file mode 100644 index 0000000..2cb8478 --- /dev/null +++ b/api/billing/fees/internal/server/server.go @@ -0,0 +1,12 @@ +package server + +import ( + serverimp "github.com/tech/sendico/billing/fees/internal/server/internal" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" +) + +// Create constructs the billing fees server implementation. +func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return serverimp.Create(logger, file, debug) +} diff --git a/api/billing/fees/internal/service/fees/calculator.go b/api/billing/fees/internal/service/fees/calculator.go new file mode 100644 index 0000000..ce7b4eb --- /dev/null +++ b/api/billing/fees/internal/service/fees/calculator.go @@ -0,0 +1,449 @@ +package fees + +import ( + "context" + "errors" + "math/big" + "sort" + "strconv" + "strings" + "time" + + "github.com/tech/sendico/billing/fees/storage/model" + oracleclient "github.com/tech/sendico/fx/oracle/client" + dmath "github.com/tech/sendico/pkg/decimal" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + "go.uber.org/zap" +) + +// Calculator isolates fee rule evaluation logic so it can be reused and tested. +type Calculator interface { + Compute(ctx context.Context, plan *model.FeePlan, intent *feesv1.Intent, bookedAt time.Time, trace *tracev1.TraceContext) (*CalculationResult, error) +} + +// CalculationResult contains derived fee lines and audit metadata. +type CalculationResult struct { + Lines []*feesv1.DerivedPostingLine + Applied []*feesv1.AppliedRule + FxUsed *feesv1.FXUsed +} + +// quoteCalculator is the default Calculator implementation. +type fxOracle interface { + LatestRate(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) +} + +type quoteCalculator struct { + logger mlogger.Logger + oracle fxOracle +} + +func newQuoteCalculator(logger mlogger.Logger, oracle fxOracle) Calculator { + return "eCalculator{ + logger: logger.Named("calculator"), + oracle: oracle, + } +} + +func (c *quoteCalculator) Compute(ctx context.Context, plan *model.FeePlan, intent *feesv1.Intent, bookedAt time.Time, _ *tracev1.TraceContext) (*CalculationResult, error) { + if plan == nil { + 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 { + return nil, merrors.InvalidArgument("invalid base amount") + } + 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)) + copy(rules, plan.Rules) + sort.SliceStable(rules, func(i, j int) bool { + if rules[i].Priority == rules[j].Priority { + return rules[i].RuleID < rules[j].RuleID + } + return rules[i].Priority < rules[j].Priority + }) + + lines := make([]*feesv1.DerivedPostingLine, 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 { + if !shouldApplyRule(rule, trigger, intent.GetAttributes(), bookedAt) { + continue + } + + ledgerAccountRef := strings.TrimSpace(rule.LedgerAccountRef) + if ledgerAccountRef == "" { + c.logger.Warn("fee rule missing ledger account reference", zap.String("rule_id", rule.RuleID)) + continue + } + + amount, scale, calcErr := c.calculateRuleAmount(baseAmount, baseScale, rule) + if calcErr != nil { + if !errors.Is(calcErr, merrors.ErrInvalidArg) { + c.logger.Warn("failed to calculate fee rule amount", zap.String("rule_id", rule.RuleID), zap.Error(calcErr)) + } + continue + } + if amount.Sign() == 0 { + continue + } + + currency := intent.GetBaseAmount().GetCurrency() + if override := strings.TrimSpace(rule.Currency); override != "" { + currency = override + } + + entrySide := mapEntrySide(rule.EntrySide) + if entrySide == accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED { + entrySide = accountingv1.EntrySide_ENTRY_SIDE_CREDIT + } + + 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 + if trigger == model.TriggerFXConversion && c.oracle != nil { + fxUsed = c.buildFxUsed(ctx, intent) + } + + return &CalculationResult{ + Lines: lines, + Applied: applied, + FxUsed: fxUsed, + }, nil +} + +func (c *quoteCalculator) calculateRuleAmount(baseAmount *big.Rat, baseScale uint32, rule model.FeeRule) (*big.Rat, uint32, error) { + scale, err := resolveRuleScale(rule, baseScale) + if err != nil { + return nil, 0, err + } + + result := new(big.Rat) + + if percentage := strings.TrimSpace(rule.Percentage); percentage != "" { + percentageRat, perr := dmath.RatFromString(percentage) + if perr != nil { + return nil, 0, merrors.InvalidArgument("invalid percentage") + } + result = dmath.AddRat(result, dmath.MulRat(baseAmount, percentageRat)) + } + + if fixed := strings.TrimSpace(rule.FixedAmount); fixed != "" { + fixedRat, ferr := dmath.RatFromString(fixed) + if ferr != nil { + return nil, 0, merrors.InvalidArgument("invalid fixed amount") + } + result = dmath.AddRat(result, fixedRat) + } + + if minStr := strings.TrimSpace(rule.MinimumAmount); minStr != "" { + minRat, merr := dmath.RatFromString(minStr) + if merr != nil { + 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 != "" { + maxRat, merr := dmath.RatFromString(maxStr) + if merr != nil { + return nil, 0, merrors.InvalidArgument("invalid maximum amount") + } + if dmath.CmpRat(result, maxRat) > 0 { + result = new(big.Rat).Set(maxRat) + } + } + + if result.Sign() < 0 { + result = new(big.Rat).Abs(result) + } + + rounded, rerr := dmath.RoundRatToScale(result, scale, toDecimalRounding(rule.Rounding)) + if rerr != nil { + return nil, 0, rerr + } + + return rounded, scale, nil +} + +const ( + attrFxBaseCurrency = "fx_base_currency" + attrFxQuoteCurrency = "fx_quote_currency" + attrFxProvider = "fx_provider" + attrFxSide = "fx_side" + attrFxRateOverride = "fx_rate" +) + +func (c *quoteCalculator) buildFxUsed(ctx context.Context, intent *feesv1.Intent) *feesv1.FXUsed { + if intent == nil || c.oracle == nil { + return nil + } + + attrs := intent.GetAttributes() + base := strings.TrimSpace(attrs[attrFxBaseCurrency]) + quote := strings.TrimSpace(attrs[attrFxQuoteCurrency]) + if base == "" || quote == "" { + return nil + } + + pair := &fxv1.CurrencyPair{Base: base, Quote: quote} + provider := strings.TrimSpace(attrs[attrFxProvider]) + + snapshot, err := c.oracle.LatestRate(ctx, oracleclient.LatestRateParams{ + Meta: oracleclient.RequestMeta{}, + Pair: pair, + Provider: provider, + }) + if err != nil { + c.logger.Warn("fees: failed to fetch FX context", zap.Error(err)) + return nil + } + if snapshot == nil { + return nil + } + + rateValue := strings.TrimSpace(attrs[attrFxRateOverride]) + if rateValue == "" { + rateValue = snapshot.Mid + } + if rateValue == "" { + rateValue = snapshot.Ask + } + if rateValue == "" { + rateValue = snapshot.Bid + } + + return &feesv1.FXUsed{ + Pair: pair, + Side: parseFxSide(strings.TrimSpace(attrs[attrFxSide])), + Rate: &moneyv1.Decimal{Value: rateValue}, + AsofUnixMs: snapshot.AsOf.UnixMilli(), + Provider: snapshot.Provider, + RateRef: snapshot.RateRef, + SpreadBps: &moneyv1.Decimal{Value: snapshot.SpreadBps}, + } +} + +func parseFxSide(value string) fxv1.Side { + switch strings.ToLower(value) { + case "buy_base", "buy_base_sell_quote", "buy": + return fxv1.Side_BUY_BASE_SELL_QUOTE + case "sell_base", "sell_base_buy_quote", "sell": + return fxv1.Side_SELL_BASE_BUY_QUOTE + default: + return fxv1.Side_SIDE_UNSPECIFIED + } +} + +func inferScale(amount string) uint32 { + value := strings.TrimSpace(amount) + if value == "" { + return 0 + } + if idx := strings.IndexAny(value, "eE"); idx >= 0 { + value = value[:idx] + } + if strings.HasPrefix(value, "+") || strings.HasPrefix(value, "-") { + value = value[1:] + } + if dot := strings.IndexByte(value, '.'); dot >= 0 { + return uint32(len(value[dot+1:])) + } + return 0 +} + +func shouldApplyRule(rule model.FeeRule, trigger model.Trigger, attributes map[string]string, bookedAt time.Time) bool { + if rule.Trigger != trigger { + return false + } + if rule.EffectiveFrom.After(bookedAt) { + return false + } + if rule.EffectiveTo != nil && rule.EffectiveTo.Before(bookedAt) { + return false + } + return ruleMatchesAttributes(rule, attributes) +} + +func resolveRuleScale(rule model.FeeRule, fallback uint32) (uint32, error) { + if rule.Metadata != nil { + for _, field := range []string{"scale", "decimals", "precision"} { + if value, ok := rule.Metadata[field]; ok && strings.TrimSpace(value) != "" { + return parseScale(field, value) + } + } + } + return fallback, nil +} + +func parseScale(field, value string) (uint32, error) { + clean := strings.TrimSpace(value) + if clean == "" { + return 0, merrors.InvalidArgument(field + " is empty") + } + parsed, err := strconv.ParseUint(clean, 10, 32) + if err != nil { + return 0, merrors.InvalidArgument("invalid " + field + " value") + } + return uint32(parsed), nil +} + +func metadataValue(meta map[string]string, key string) string { + if meta == nil { + return "" + } + return strings.TrimSpace(meta[key]) +} + +func cloneStringMap(src map[string]string) map[string]string { + if len(src) == 0 { + return nil + } + cloned := make(map[string]string, len(src)) + for k, v := range src { + cloned[k] = v + } + return cloned +} + +func ruleMatchesAttributes(rule model.FeeRule, attributes map[string]string) bool { + if len(rule.AppliesTo) == 0 { + return true + } + for key, value := range rule.AppliesTo { + if attributes == nil { + return false + } + if attrValue, ok := attributes[key]; !ok || attrValue != value { + return false + } + } + return true +} + +func convertTrigger(trigger feesv1.Trigger) model.Trigger { + switch trigger { + case feesv1.Trigger_TRIGGER_CAPTURE: + return model.TriggerCapture + case feesv1.Trigger_TRIGGER_REFUND: + return model.TriggerRefund + case feesv1.Trigger_TRIGGER_DISPUTE: + return model.TriggerDispute + case feesv1.Trigger_TRIGGER_PAYOUT: + return model.TriggerPayout + case feesv1.Trigger_TRIGGER_FX_CONVERSION: + return model.TriggerFXConversion + default: + return model.TriggerUnspecified + } +} + +func mapLineType(lineType string) accountingv1.PostingLineType { + switch strings.ToLower(lineType) { + case "tax": + return accountingv1.PostingLineType_POSTING_LINE_TAX + case "spread": + return accountingv1.PostingLineType_POSTING_LINE_SPREAD + case "reversal": + return accountingv1.PostingLineType_POSTING_LINE_REVERSAL + default: + return accountingv1.PostingLineType_POSTING_LINE_FEE + } +} + +func mapEntrySide(entrySide string) accountingv1.EntrySide { + switch strings.ToLower(entrySide) { + case "debit": + return accountingv1.EntrySide_ENTRY_SIDE_DEBIT + case "credit": + return accountingv1.EntrySide_ENTRY_SIDE_CREDIT + default: + return accountingv1.EntrySide_ENTRY_SIDE_UNSPECIFIED + } +} + +func toDecimalRounding(mode string) dmath.RoundingMode { + switch strings.ToLower(strings.TrimSpace(mode)) { + case "half_up": + return dmath.RoundingModeHalfUp + case "down": + return dmath.RoundingModeDown + case "half_even", "bankers": + return dmath.RoundingModeHalfEven + default: + return dmath.RoundingModeHalfEven + } +} + +func mapRoundingMode(mode string) moneyv1.RoundingMode { + switch strings.ToLower(mode) { + case "half_up": + return moneyv1.RoundingMode_ROUND_HALF_UP + case "down": + return moneyv1.RoundingMode_ROUND_DOWN + default: + return moneyv1.RoundingMode_ROUND_HALF_EVEN + } +} diff --git a/api/billing/fees/internal/service/fees/metrics.go b/api/billing/fees/internal/service/fees/metrics.go new file mode 100644 index 0000000..3f73713 --- /dev/null +++ b/api/billing/fees/internal/service/fees/metrics.go @@ -0,0 +1,71 @@ +package fees + +import ( + "strconv" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + metricsOnce sync.Once + + quoteRequestsTotal *prometheus.CounterVec + quoteLatency *prometheus.HistogramVec +) + +func initMetrics() { + metricsOnce.Do(func() { + quoteRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "billing", + Subsystem: "fees", + Name: "requests_total", + Help: "Total number of fee service requests processed.", + }, + []string{"call", "trigger", "status", "fx_used"}, + ) + + quoteLatency = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "billing", + Subsystem: "fees", + Name: "request_latency_seconds", + Help: "Latency of fee service requests.", + Buckets: prometheus.DefBuckets, + }, + []string{"call", "trigger", "status", "fx_used"}, + ) + }) +} + +func observeMetrics(call string, trigger feesv1.Trigger, statusLabel string, fxUsed bool, took time.Duration) { + triggerLabel := trigger.String() + if trigger == feesv1.Trigger_TRIGGER_UNSPECIFIED { + triggerLabel = "TRIGGER_UNSPECIFIED" + } + fxLabel := strconv.FormatBool(fxUsed) + quoteRequestsTotal.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Inc() + quoteLatency.WithLabelValues(call, triggerLabel, statusLabel, fxLabel).Observe(took.Seconds()) +} + +func statusFromError(err error) string { + if err == nil { + return "success" + } + st, ok := status.FromError(err) + if !ok { + return "error" + } + code := st.Code() + if code == codes.OK { + return "success" + } + return strings.ToLower(code.String()) +} diff --git a/api/billing/fees/internal/service/fees/options.go b/api/billing/fees/internal/service/fees/options.go new file mode 100644 index 0000000..4982e99 --- /dev/null +++ b/api/billing/fees/internal/service/fees/options.go @@ -0,0 +1,37 @@ +package fees + +import ( + oracleclient "github.com/tech/sendico/fx/oracle/client" + clockpkg "github.com/tech/sendico/pkg/clock" +) + +// Option configures a Service instance. +type Option func(*Service) + +// WithClock sets a custom clock implementation. +func WithClock(clock clockpkg.Clock) Option { + return func(s *Service) { + if clock != nil { + s.clock = clock + } + } +} + +// WithCalculator sets a custom calculator implementation. +func WithCalculator(calculator Calculator) Option { + return func(s *Service) { + if calculator != nil { + s.calculator = calculator + } + } +} + +// WithOracleClient wires an FX oracle client for FX trigger evaluations. +func WithOracleClient(oracle oracleclient.Client) Option { + return func(s *Service) { + s.oracle = oracle + if qc, ok := s.calculator.(*quoteCalculator); ok { + qc.oracle = oracle + } + } +} diff --git a/api/billing/fees/internal/service/fees/service.go b/api/billing/fees/internal/service/fees/service.go new file mode 100644 index 0000000..c5b2ad6 --- /dev/null +++ b/api/billing/fees/internal/service/fees/service.go @@ -0,0 +1,322 @@ +package fees + +import ( + "context" + "encoding/base64" + "encoding/json" + "errors" + "strings" + "time" + + "github.com/tech/sendico/billing/fees/storage" + oracleclient "github.com/tech/sendico/fx/oracle/client" + "github.com/tech/sendico/pkg/api/routers" + clockpkg "github.com/tech/sendico/pkg/clock" + "github.com/tech/sendico/pkg/merrors" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" +) + +type Service struct { + logger mlogger.Logger + storage storage.Repository + producer msg.Producer + clock clockpkg.Clock + calculator Calculator + oracle oracleclient.Client + feesv1.UnimplementedFeeEngineServer +} + +func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service { + svc := &Service{ + logger: logger.Named("fees"), + storage: repo, + producer: producer, + clock: clockpkg.NewSystem(), + } + initMetrics() + + for _, opt := range opts { + opt(svc) + } + + if svc.clock == nil { + svc.clock = clockpkg.NewSystem() + } + if svc.calculator == nil { + svc.calculator = newQuoteCalculator(svc.logger, svc.oracle) + } + + return svc +} + +func (s *Service) Register(router routers.GRPC) error { + return router.Register(func(reg grpc.ServiceRegistrar) { + feesv1.RegisterFeeEngineServer(reg, s) + }) +} + +func (s *Service) QuoteFees(ctx context.Context, req *feesv1.QuoteFeesRequest) (resp *feesv1.QuoteFeesResponse, err error) { + start := s.clock.Now() + trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED + if req != nil && req.GetIntent() != nil { + trigger = req.GetIntent().GetTrigger() + } + var fxUsed bool + defer func() { + statusLabel := statusFromError(err) + if err == nil && resp != nil { + fxUsed = resp.GetFxUsed() != nil + } + observeMetrics("quote", trigger, statusLabel, fxUsed, time.Since(start)) + }() + + if err = s.validateQuoteRequest(req); err != nil { + return nil, err + } + + orgRef, parseErr := primitive.ObjectIDFromHex(req.GetMeta().GetOrganizationRef()) + if parseErr != nil { + err = status.Error(codes.InvalidArgument, "invalid organization_ref") + return nil, err + } + + lines, applied, fx, computeErr := s.computeQuote(ctx, orgRef, req.GetIntent(), req.GetPolicy(), req.GetMeta().GetTrace()) + if computeErr != nil { + err = computeErr + return nil, err + } + + resp = &feesv1.QuoteFeesResponse{ + Meta: &feesv1.ResponseMeta{Trace: req.GetMeta().GetTrace()}, + Lines: lines, + Applied: applied, + FxUsed: fx, + } + return resp, nil +} + +func (s *Service) PrecomputeFees(ctx context.Context, req *feesv1.PrecomputeFeesRequest) (resp *feesv1.PrecomputeFeesResponse, err error) { + start := s.clock.Now() + trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED + if req != nil && req.GetIntent() != nil { + trigger = req.GetIntent().GetTrigger() + } + var fxUsed bool + defer func() { + statusLabel := statusFromError(err) + if err == nil && resp != nil { + fxUsed = resp.GetFxUsed() != nil + } + observeMetrics("precompute", trigger, statusLabel, fxUsed, time.Since(start)) + }() + + if err = s.validatePrecomputeRequest(req); err != nil { + return nil, err + } + + now := s.clock.Now() + + orgRef, parseErr := primitive.ObjectIDFromHex(req.GetMeta().GetOrganizationRef()) + if parseErr != nil { + err = status.Error(codes.InvalidArgument, "invalid organization_ref") + return nil, err + } + + lines, applied, fx, computeErr := s.computeQuoteWithTime(ctx, orgRef, req.GetIntent(), nil, req.GetMeta().GetTrace(), now) + if computeErr != nil { + err = computeErr + return nil, err + } + + ttl := req.GetTtlMs() + if ttl <= 0 { + ttl = 60000 + } + expiresAt := now.Add(time.Duration(ttl) * time.Millisecond) + + payload := feeQuoteTokenPayload{ + OrganizationRef: req.GetMeta().GetOrganizationRef(), + Intent: req.GetIntent(), + ExpiresAtUnixMs: expiresAt.UnixMilli(), + Trace: req.GetMeta().GetTrace(), + } + + var token string + if token, err = encodeTokenPayload(payload); err != nil { + s.logger.Warn("failed to encode fee quote token", zap.Error(err)) + err = status.Error(codes.Internal, "failed to encode fee quote token") + return nil, err + } + + resp = &feesv1.PrecomputeFeesResponse{ + Meta: &feesv1.ResponseMeta{Trace: req.GetMeta().GetTrace()}, + FeeQuoteToken: token, + ExpiresAt: timestamppb.New(expiresAt), + Lines: lines, + Applied: applied, + FxUsed: fx, + } + return resp, nil +} + +func (s *Service) ValidateFeeToken(ctx context.Context, req *feesv1.ValidateFeeTokenRequest) (resp *feesv1.ValidateFeeTokenResponse, err error) { + start := s.clock.Now() + trigger := feesv1.Trigger_TRIGGER_UNSPECIFIED + var fxUsed bool + 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)) + }() + + if req == nil || strings.TrimSpace(req.GetFeeQuoteToken()) == "" { + err = status.Error(codes.InvalidArgument, "fee_quote_token is required") + return nil, err + } + + now := s.clock.Now() + + payload, decodeErr := decodeTokenPayload(req.GetFeeQuoteToken()) + if decodeErr != nil { + s.logger.Warn("failed to decode fee quote token", zap.Error(decodeErr)) + resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} + return resp, nil + } + + trigger = payload.Intent.GetTrigger() + + if now.UnixMilli() > payload.ExpiresAtUnixMs { + resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "expired"} + return resp, nil + } + + orgRef, parseErr := primitive.ObjectIDFromHex(payload.OrganizationRef) + if parseErr != nil { + s.logger.Warn("token contained invalid organization reference", zap.Error(parseErr)) + resp = &feesv1.ValidateFeeTokenResponse{Meta: &feesv1.ResponseMeta{}, Valid: false, Reason: "invalid_token"} + return resp, nil + } + + lines, applied, fx, computeErr := s.computeQuoteWithTime(ctx, orgRef, payload.Intent, nil, payload.Trace, now) + if computeErr != nil { + err = computeErr + return nil, err + } + + resp = &feesv1.ValidateFeeTokenResponse{ + Meta: &feesv1.ResponseMeta{Trace: payload.Trace}, + Valid: true, + Intent: payload.Intent, + Lines: lines, + Applied: applied, + FxUsed: fx, + } + return resp, nil +} + +func (s *Service) validateQuoteRequest(req *feesv1.QuoteFeesRequest) error { + if req == nil { + return status.Error(codes.InvalidArgument, "request is required") + } + if req.GetMeta() == nil || strings.TrimSpace(req.GetMeta().GetOrganizationRef()) == "" { + return status.Error(codes.InvalidArgument, "meta.organization_ref is required") + } + if req.GetIntent() == nil { + return status.Error(codes.InvalidArgument, "intent is required") + } + if req.GetIntent().GetTrigger() == feesv1.Trigger_TRIGGER_UNSPECIFIED { + return status.Error(codes.InvalidArgument, "intent.trigger is required") + } + if req.GetIntent().GetBaseAmount() == nil { + return status.Error(codes.InvalidArgument, "intent.base_amount is required") + } + if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetAmount()) == "" { + return status.Error(codes.InvalidArgument, "intent.base_amount.amount is required") + } + if strings.TrimSpace(req.GetIntent().GetBaseAmount().GetCurrency()) == "" { + return status.Error(codes.InvalidArgument, "intent.base_amount.currency is required") + } + return nil +} + +func (s *Service) validatePrecomputeRequest(req *feesv1.PrecomputeFeesRequest) error { + if req == nil { + return status.Error(codes.InvalidArgument, "request is required") + } + return s.validateQuoteRequest(&feesv1.QuoteFeesRequest{Meta: req.GetMeta(), Intent: req.GetIntent()}) +} + +func (s *Service) computeQuote(ctx context.Context, orgRef primitive.ObjectID, intent *feesv1.Intent, overrides *feesv1.PolicyOverrides, trace *tracev1.TraceContext) ([]*feesv1.DerivedPostingLine, []*feesv1.AppliedRule, *feesv1.FXUsed, error) { + return s.computeQuoteWithTime(ctx, orgRef, intent, overrides, trace, s.clock.Now()) +} + +func (s *Service) computeQuoteWithTime(ctx context.Context, orgRef primitive.ObjectID, intent *feesv1.Intent, overrides *feesv1.PolicyOverrides, trace *tracev1.TraceContext, now time.Time) ([]*feesv1.DerivedPostingLine, []*feesv1.AppliedRule, *feesv1.FXUsed, error) { + bookedAt := now + if intent.GetBookedAt() != nil && intent.GetBookedAt().IsValid() { + bookedAt = intent.GetBookedAt().AsTime() + } + + plan, err := s.storage.Plans().GetActivePlan(ctx, orgRef, bookedAt) + if err != nil { + if errors.Is(err, storage.ErrFeePlanNotFound) { + return nil, nil, nil, status.Error(codes.NotFound, "fee plan not found") + } + s.logger.Warn("failed to load active fee plan", zap.Error(err)) + return nil, nil, nil, status.Error(codes.Internal, "failed to load fee plan") + } + + result, calcErr := s.calculator.Compute(ctx, plan, intent, bookedAt, trace) + if calcErr != nil { + if errors.Is(calcErr, merrors.ErrInvalidArg) { + return nil, nil, nil, status.Error(codes.InvalidArgument, calcErr.Error()) + } + s.logger.Warn("failed to compute fee quote", zap.Error(calcErr)) + return nil, nil, nil, status.Error(codes.Internal, "failed to compute fee quote") + } + + return result.Lines, result.Applied, result.FxUsed, nil +} + +type feeQuoteTokenPayload struct { + OrganizationRef string `json:"organization_ref"` + Intent *feesv1.Intent `json:"intent"` + ExpiresAtUnixMs int64 `json:"expires_at_unix_ms"` + Trace *tracev1.TraceContext `json:"trace,omitempty"` +} + +func encodeTokenPayload(payload feeQuoteTokenPayload) (string, error) { + data, err := json.Marshal(payload) + if err != nil { + return "", merrors.Internal("fees: failed to serialize token payload") + } + return base64.StdEncoding.EncodeToString(data), nil +} + +func decodeTokenPayload(token string) (feeQuoteTokenPayload, error) { + var payload feeQuoteTokenPayload + data, err := base64.StdEncoding.DecodeString(token) + if err != nil { + return payload, merrors.InvalidArgument("fees: invalid token encoding") + } + if err := json.Unmarshal(data, &payload); err != nil { + return payload, merrors.InvalidArgument("fees: invalid token payload") + } + return payload, nil +} diff --git a/api/billing/fees/internal/service/fees/service_test.go b/api/billing/fees/internal/service/fees/service_test.go new file mode 100644 index 0000000..bb3ea24 --- /dev/null +++ b/api/billing/fees/internal/service/fees/service_test.go @@ -0,0 +1,476 @@ +package fees + +import ( + "context" + "testing" + "time" + + "github.com/tech/sendico/billing/fees/storage" + "github.com/tech/sendico/billing/fees/storage/model" + oracleclient "github.com/tech/sendico/fx/oracle/client" + me "github.com/tech/sendico/pkg/messaging/envelope" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestQuoteFees_ComputesDerivedLines(t *testing.T) { + t.Helper() + + now := time.Date(2024, 1, 10, 16, 0, 0, 0, time.UTC) + orgRef := primitive.NewObjectID() + + plan := &model.FeePlan{ + Active: true, + EffectiveFrom: now.Add(-time.Hour), + Rules: []model.FeeRule{ + { + RuleID: "capture_default", + Trigger: model.TriggerCapture, + Priority: 10, + Percentage: "0.029", + FixedAmount: "0.30", + LedgerAccountRef: "acct:fees", + LineType: "fee", + EntrySide: "credit", + Rounding: "half_up", + Metadata: map[string]string{ + "scale": "2", + "tax_code": "VAT", + "tax_rate": "0.20", + }, + EffectiveFrom: now.Add(-time.Hour), + }, + }, + } + plan.SetID(primitive.NewObjectID()) + plan.SetOrganizationRef(orgRef) + + service := NewService( + zap.NewNop(), + &stubRepository{plans: &stubPlansStore{plan: plan}}, + noopProducer{}, + WithClock(fixedClock{now: now}), + ) + + req := &feesv1.QuoteFeesRequest{ + Meta: &feesv1.RequestMeta{ + OrganizationRef: orgRef.Hex(), + Trace: &tracev1.TraceContext{ + TraceRef: "trace-capture", + }, + }, + Intent: &feesv1.Intent{ + Trigger: feesv1.Trigger_TRIGGER_CAPTURE, + BaseAmount: &moneyv1.Money{ + Amount: "100.00", + Currency: "USD", + }, + BookedAt: timestamppb.New(now), + Attributes: map[string]string{"channel": "card"}, + }, + } + + resp, err := service.QuoteFees(context.Background(), req) + if err != nil { + t.Fatalf("QuoteFees returned error: %v", err) + } + + if resp.GetMeta().GetTrace().GetTraceRef() != "trace-capture" { + t.Fatalf("expected trace_ref to round-trip, got %q", resp.GetMeta().GetTrace().GetTraceRef()) + } + + if len(resp.GetLines()) != 1 { + t.Fatalf("expected 1 derived line, got %d", len(resp.GetLines())) + } + + line := resp.GetLines()[0] + if got := line.GetMoney().GetAmount(); got != "3.20" { + t.Fatalf("expected fee amount 3.20, got %s", got) + } + if line.GetMoney().GetCurrency() != "USD" { + t.Fatalf("expected currency USD, got %s", line.GetMoney().GetCurrency()) + } + if line.GetLedgerAccountRef() != "acct:fees" { + t.Fatalf("unexpected ledger account ref %s", line.GetLedgerAccountRef()) + } + if meta := line.GetMeta(); meta["fee_rule_id"] != "capture_default" || meta["fee_plan_id"] != plan.GetID().Hex() || meta["tax_code"] != "VAT" { + t.Fatalf("unexpected derived line metadata: %#v", meta) + } + + if len(resp.GetApplied()) != 1 { + t.Fatalf("expected 1 applied rule, got %d", len(resp.GetApplied())) + } + + applied := resp.GetApplied()[0] + if applied.GetTaxCode() != "VAT" || applied.GetTaxRate() != "0.20" { + t.Fatalf("applied rule metadata mismatch: %+v", applied) + } + if applied.GetRounding() != moneyv1.RoundingMode_ROUND_HALF_UP { + t.Fatalf("expected rounding HALF_UP, got %v", applied.GetRounding()) + } + if applied.GetParameters()["scale"] != "2" { + t.Fatalf("expected parameters to carry metadata scale, got %+v", applied.GetParameters()) + } +} + +func TestQuoteFees_FiltersByAttributesAndDates(t *testing.T) { + t.Helper() + + now := time.Date(2024, 5, 20, 9, 30, 0, 0, time.UTC) + orgRef := primitive.NewObjectID() + + plan := &model.FeePlan{ + Active: true, + EffectiveFrom: now.Add(-24 * time.Hour), + Rules: []model.FeeRule{ + { + RuleID: "base", + Trigger: model.TriggerCapture, + Priority: 1, + Percentage: "0.10", + LedgerAccountRef: "acct:base", + Metadata: map[string]string{"scale": "2"}, + Rounding: "half_even", + EffectiveFrom: now.Add(-time.Hour), + }, + { + RuleID: "future", + Trigger: model.TriggerCapture, + Priority: 2, + Percentage: "0.50", + LedgerAccountRef: "acct:future", + Metadata: map[string]string{"scale": "2"}, + Rounding: "half_even", + EffectiveFrom: now.Add(time.Hour), + }, + { + RuleID: "attr", + Trigger: model.TriggerCapture, + Priority: 3, + Percentage: "0.30", + LedgerAccountRef: "acct:attr", + Metadata: map[string]string{"scale": "2"}, + AppliesTo: map[string]string{"region": "eu"}, + Rounding: "half_even", + EffectiveFrom: now.Add(-time.Hour), + }, + }, + } + plan.SetID(primitive.NewObjectID()) + plan.SetOrganizationRef(orgRef) + + service := NewService( + zap.NewNop(), + &stubRepository{plans: &stubPlansStore{plan: plan}}, + noopProducer{}, + WithClock(fixedClock{now: now}), + ) + + req := &feesv1.QuoteFeesRequest{ + Meta: &feesv1.RequestMeta{OrganizationRef: orgRef.Hex()}, + Intent: &feesv1.Intent{ + Trigger: feesv1.Trigger_TRIGGER_CAPTURE, + BaseAmount: &moneyv1.Money{ + Amount: "50.00", + Currency: "EUR", + }, + BookedAt: timestamppb.New(now), + Attributes: map[string]string{"region": "us"}, + }, + } + + resp, err := service.QuoteFees(context.Background(), req) + if err != nil { + t.Fatalf("QuoteFees returned error: %v", err) + } + if len(resp.GetLines()) != 1 { + t.Fatalf("expected only base rule to fire, got %d lines", len(resp.GetLines())) + } + line := resp.GetLines()[0] + if line.GetLedgerAccountRef() != "acct:base" { + t.Fatalf("expected base rule to apply, got %s", line.GetLedgerAccountRef()) + } + if line.GetMoney().GetAmount() != "5.00" { + t.Fatalf("expected 5.00 amount, got %s", line.GetMoney().GetAmount()) + } +} + +func TestQuoteFees_RoundingDown(t *testing.T) { + t.Helper() + + now := time.Date(2024, 3, 15, 12, 0, 0, 0, time.UTC) + orgRef := primitive.NewObjectID() + + plan := &model.FeePlan{ + Active: true, + EffectiveFrom: now.Add(-time.Hour), + Rules: []model.FeeRule{ + { + RuleID: "round_down", + Trigger: model.TriggerCapture, + Priority: 1, + FixedAmount: "0.015", + LedgerAccountRef: "acct:round", + Metadata: map[string]string{"scale": "2"}, + Rounding: "down", + EffectiveFrom: now.Add(-time.Hour), + }, + }, + } + plan.SetID(primitive.NewObjectID()) + plan.SetOrganizationRef(orgRef) + + service := NewService( + zap.NewNop(), + &stubRepository{plans: &stubPlansStore{plan: plan}}, + noopProducer{}, + WithClock(fixedClock{now: now}), + ) + + req := &feesv1.QuoteFeesRequest{ + Meta: &feesv1.RequestMeta{OrganizationRef: orgRef.Hex()}, + Intent: &feesv1.Intent{ + Trigger: feesv1.Trigger_TRIGGER_CAPTURE, + BaseAmount: &moneyv1.Money{ + Amount: "1.00", + Currency: "USD", + }, + BookedAt: timestamppb.New(now), + }, + } + + resp, err := service.QuoteFees(context.Background(), req) + if err != nil { + t.Fatalf("QuoteFees returned error: %v", err) + } + if len(resp.GetLines()) != 1 { + t.Fatalf("expected single derived line, got %d", len(resp.GetLines())) + } + if resp.GetLines()[0].GetMoney().GetAmount() != "0.01" { + t.Fatalf("expected rounding down to 0.01, got %s", resp.GetLines()[0].GetMoney().GetAmount()) + } +} + +func TestQuoteFees_UsesInjectedCalculator(t *testing.T) { + t.Helper() + + now := time.Date(2024, 6, 1, 8, 0, 0, 0, time.UTC) + orgRef := primitive.NewObjectID() + plan := &model.FeePlan{ + Active: true, + EffectiveFrom: now.Add(-time.Hour), + } + plan.SetID(primitive.NewObjectID()) + plan.SetOrganizationRef(orgRef) + + result := &CalculationResult{ + Lines: []*feesv1.DerivedPostingLine{ + { + LedgerAccountRef: "acct:stub", + Money: &moneyv1.Money{ + Amount: "1.23", + Currency: "USD", + }, + }, + }, + Applied: []*feesv1.AppliedRule{ + {RuleId: "stub"}, + }, + } + calc := &stubCalculator{result: result} + + service := NewService( + zap.NewNop(), + &stubRepository{plans: &stubPlansStore{plan: plan}}, + noopProducer{}, + WithClock(fixedClock{now: now}), + WithCalculator(calc), + ) + + resp, err := service.QuoteFees(context.Background(), &feesv1.QuoteFeesRequest{ + Meta: &feesv1.RequestMeta{OrganizationRef: orgRef.Hex()}, + Intent: &feesv1.Intent{ + Trigger: feesv1.Trigger_TRIGGER_CAPTURE, + BaseAmount: &moneyv1.Money{ + Amount: "10.00", + Currency: "USD", + }, + }, + }) + if err != nil { + t.Fatalf("QuoteFees returned error: %v", err) + } + if !calc.called { + t.Fatalf("expected calculator to be invoked") + } + if calc.gotPlan != plan { + t.Fatalf("expected calculator to receive plan pointer") + } + if len(resp.GetLines()) != len(result.Lines) { + t.Fatalf("expected %d lines, got %d", len(result.Lines), len(resp.GetLines())) + } + if resp.GetLines()[0].GetLedgerAccountRef() != "acct:stub" { + t.Fatalf("unexpected ledger account in response: %s", resp.GetLines()[0].GetLedgerAccountRef()) + } +} + +func TestQuoteFees_PopulatesFxUsed(t *testing.T) { + t.Helper() + + now := time.Date(2024, 7, 1, 9, 30, 0, 0, time.UTC) + orgRef := primitive.NewObjectID() + + plan := &model.FeePlan{ + Active: true, + EffectiveFrom: now.Add(-time.Hour), + Rules: []model.FeeRule{ + { + RuleID: "fx_mark_up", + Trigger: model.TriggerFXConversion, + Priority: 1, + Percentage: "0.03", + LedgerAccountRef: "acct:fx", + Metadata: map[string]string{"scale": "2"}, + Rounding: "half_even", + EffectiveFrom: now.Add(-time.Hour), + }, + }, + } + plan.SetID(primitive.NewObjectID()) + plan.SetOrganizationRef(orgRef) + + fakeOracle := &oracleclient.Fake{ + LatestRateFn: func(ctx context.Context, req oracleclient.LatestRateParams) (*oracleclient.RateSnapshot, error) { + return &oracleclient.RateSnapshot{ + Pair: req.Pair, + Mid: "1.2300", + SpreadBps: "12", + Provider: "TestProvider", + RateRef: "rate-ref-123", + AsOf: now.Add(-2 * time.Minute), + }, nil + }, + } + + service := NewService( + zap.NewNop(), + &stubRepository{plans: &stubPlansStore{plan: plan}}, + noopProducer{}, + WithClock(fixedClock{now: now}), + WithOracleClient(fakeOracle), + ) + + resp, err := service.QuoteFees(context.Background(), &feesv1.QuoteFeesRequest{ + Meta: &feesv1.RequestMeta{OrganizationRef: orgRef.Hex()}, + Intent: &feesv1.Intent{ + Trigger: feesv1.Trigger_TRIGGER_FX_CONVERSION, + BaseAmount: &moneyv1.Money{ + Amount: "100.00", + Currency: "USD", + }, + Attributes: map[string]string{ + "fx_base_currency": "USD", + "fx_quote_currency": "EUR", + "fx_provider": "TestProvider", + "fx_side": "buy_base", + }, + }, + }) + if err != nil { + t.Fatalf("QuoteFees returned error: %v", err) + } + + if resp.GetFxUsed() == nil { + t.Fatalf("expected FxUsed to be populated") + } + fx := resp.GetFxUsed() + if fx.GetProvider() != "TestProvider" || fx.GetRate().GetValue() != "1.2300" { + t.Fatalf("unexpected FxUsed payload: %+v", fx) + } + if fx.GetPair().GetBase() != "USD" || fx.GetPair().GetQuote() != "EUR" { + t.Fatalf("unexpected currency pair: %+v", fx.GetPair()) + } +} + +type stubRepository struct { + plans storage.PlansStore +} + +func (s *stubRepository) Ping(context.Context) error { + return nil +} + +func (s *stubRepository) Plans() storage.PlansStore { + return s.plans +} + +type stubPlansStore struct { + plan *model.FeePlan +} + +func (s *stubPlansStore) Create(context.Context, *model.FeePlan) error { + return nil +} + +func (s *stubPlansStore) Update(context.Context, *model.FeePlan) error { + return nil +} + +func (s *stubPlansStore) Get(context.Context, primitive.ObjectID) (*model.FeePlan, error) { + return nil, storage.ErrFeePlanNotFound +} + +func (s *stubPlansStore) GetActivePlan(_ context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) { + if s.plan == nil { + return nil, storage.ErrFeePlanNotFound + } + if s.plan.GetOrganizationRef() != orgRef { + return nil, storage.ErrFeePlanNotFound + } + if !s.plan.Active { + return nil, storage.ErrFeePlanNotFound + } + if !s.plan.EffectiveFrom.Before(at) && !s.plan.EffectiveFrom.Equal(at) { + return nil, storage.ErrFeePlanNotFound + } + if s.plan.EffectiveTo != nil && s.plan.EffectiveTo.Before(at) { + return nil, storage.ErrFeePlanNotFound + } + return s.plan, nil +} + +type noopProducer struct{} + +func (noopProducer) SendMessage(me.Envelope) error { + return nil +} + +type fixedClock struct { + now time.Time +} + +func (f fixedClock) Now() time.Time { + return f.now +} + +type stubCalculator struct { + result *CalculationResult + err error + called bool + gotPlan *model.FeePlan + bookedAt time.Time +} + +func (s *stubCalculator) Compute(_ context.Context, plan *model.FeePlan, _ *feesv1.Intent, bookedAt time.Time, _ *tracev1.TraceContext) (*CalculationResult, error) { + s.called = true + s.gotPlan = plan + s.bookedAt = bookedAt + if s.err != nil { + return nil, s.err + } + return s.result, nil +} diff --git a/api/billing/fees/main.go b/api/billing/fees/main.go new file mode 100644 index 0000000..3e2fbf1 --- /dev/null +++ b/api/billing/fees/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/tech/sendico/billing/fees/internal/appversion" + si "github.com/tech/sendico/billing/fees/internal/server" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" + smain "github.com/tech/sendico/pkg/server/main" +) + +func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return si.Create(logger, file, debug) +} + +func main() { + smain.RunServer("main", appversion.Create(), factory) +} diff --git a/api/billing/fees/storage/model/plan.go b/api/billing/fees/storage/model/plan.go new file mode 100644 index 0000000..f539f5f --- /dev/null +++ b/api/billing/fees/storage/model/plan.go @@ -0,0 +1,62 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" +) + +const ( + FeePlansCollection = "fee_plans" +) + +// Trigger represents the event that causes a fee rule to apply. +type Trigger string + +const ( + TriggerUnspecified Trigger = "unspecified" + TriggerCapture Trigger = "capture" + TriggerRefund Trigger = "refund" + TriggerDispute Trigger = "dispute" + TriggerPayout Trigger = "payout" + TriggerFXConversion Trigger = "fx_conversion" +) + +// FeePlan describes a collection of fee rules for an organisation. +type FeePlan struct { + storable.Base `bson:",inline" json:",inline"` + model.OrganizationBoundBase `bson:",inline" json:",inline"` + model.Describable `bson:",inline" json:",inline"` + Active bool `bson:"active" json:"active"` + EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"` + EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` + Rules []FeeRule `bson:"rules,omitempty" json:"rules,omitempty"` + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// Collection implements storable.Storable. +func (*FeePlan) Collection() string { + return FeePlansCollection +} + +// FeeRule represents a single pricing rule within a plan. +type FeeRule struct { + RuleID string `bson:"ruleId" json:"ruleId"` + Trigger Trigger `bson:"trigger" json:"trigger"` + Priority int `bson:"priority" json:"priority"` + Percentage string `bson:"percentage,omitempty" json:"percentage,omitempty"` + FixedAmount string `bson:"fixedAmount,omitempty" json:"fixedAmount,omitempty"` + Currency string `bson:"currency,omitempty" json:"currency,omitempty"` + MinimumAmount string `bson:"minimumAmount,omitempty" json:"minimumAmount,omitempty"` + MaximumAmount string `bson:"maximumAmount,omitempty" json:"maximumAmount,omitempty"` + AppliesTo map[string]string `bson:"appliesTo,omitempty" json:"appliesTo,omitempty"` + Formula string `bson:"formula,omitempty" json:"formula,omitempty"` + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` + LedgerAccountRef string `bson:"ledgerAccountRef,omitempty" json:"ledgerAccountRef,omitempty"` + LineType string `bson:"lineType,omitempty" json:"lineType,omitempty"` + EntrySide string `bson:"entrySide,omitempty" json:"entrySide,omitempty"` + Rounding string `bson:"rounding,omitempty" json:"rounding,omitempty"` + EffectiveFrom time.Time `bson:"effectiveFrom" json:"effectiveFrom"` + EffectiveTo *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` +} diff --git a/api/billing/fees/storage/mongo/repository.go b/api/billing/fees/storage/mongo/repository.go new file mode 100644 index 0000000..0b2c7a6 --- /dev/null +++ b/api/billing/fees/storage/mongo/repository.go @@ -0,0 +1,69 @@ +package mongo + +import ( + "context" + "time" + + "github.com/tech/sendico/billing/fees/storage" + "github.com/tech/sendico/billing/fees/storage/mongo/store" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type Store struct { + logger mlogger.Logger + conn *db.MongoConnection + db *mongo.Database + + plans storage.PlansStore +} + +// New creates a repository backed by MongoDB for the billing fees service. +func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) { + if conn == nil { + return nil, merrors.InvalidArgument("mongo connection is nil") + } + + client := conn.Client() + if client == nil { + return nil, merrors.Internal("mongo client not initialised") + } + + database := conn.Database() + result := &Store{ + logger: logger.Named("storage").Named("mongo"), + conn: conn, + db: database, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := result.Ping(ctx); err != nil { + result.logger.Error("mongo ping failed during store init", zap.Error(err)) + return nil, err + } + + plansStore, err := store.NewPlans(result.logger, database) + if err != nil { + result.logger.Error("failed to initialise plans store", zap.Error(err)) + return nil, err + } + result.plans = plansStore + + result.logger.Info("Billing fees MongoDB storage initialised") + return result, nil +} + +func (s *Store) Ping(ctx context.Context) error { + return s.conn.Ping(ctx) +} + +func (s *Store) Plans() storage.PlansStore { + return s.plans +} + +var _ storage.Repository = (*Store)(nil) diff --git a/api/billing/fees/storage/mongo/store/plans.go b/api/billing/fees/storage/mongo/store/plans.go new file mode 100644 index 0000000..f4c8f50 --- /dev/null +++ b/api/billing/fees/storage/mongo/store/plans.go @@ -0,0 +1,144 @@ +package store + +import ( + "context" + "errors" + "time" + + "github.com/tech/sendico/billing/fees/storage" + "github.com/tech/sendico/billing/fees/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + m "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type plansStore struct { + logger mlogger.Logger + repo repository.Repository +} + +// NewPlans constructs a Mongo-backed PlansStore. +func NewPlans(logger mlogger.Logger, db *mongo.Database) (storage.PlansStore, error) { + repo := repository.CreateMongoRepository(db, mservice.FeePlans) + + // Index for organisation lookups. + orgIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: m.OrganizationRefField, Sort: ri.Asc}, + {Field: "effectiveFrom", Sort: ri.Desc}, + }, + } + if err := repo.CreateIndex(orgIndex); err != nil { + logger.Error("failed to ensure fee plan organization index", zap.Error(err)) + return nil, err + } + + // Unique index for plan versions (per organisation + effectiveFrom). + uniqueIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: m.OrganizationRefField, Sort: ri.Asc}, + {Field: "effectiveFrom", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(uniqueIndex); err != nil { + logger.Error("failed to ensure fee plan uniqueness index", zap.Error(err)) + return nil, err + } + + return &plansStore{ + logger: logger.Named("plans"), + repo: repo, + }, nil +} + +func (p *plansStore) Create(ctx context.Context, plan *model.FeePlan) error { + if plan == nil { + return merrors.InvalidArgument("plansStore: nil fee plan") + } + if err := p.repo.Insert(ctx, plan, nil); err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + return storage.ErrDuplicateFeePlan + } + p.logger.Warn("failed to create fee plan", zap.Error(err)) + return err + } + return nil +} + +func (p *plansStore) Update(ctx context.Context, plan *model.FeePlan) error { + if plan == nil || plan.GetID() == nil || plan.GetID().IsZero() { + return merrors.InvalidArgument("plansStore: invalid fee plan reference") + } + if err := p.repo.Update(ctx, plan); err != nil { + p.logger.Warn("failed to update fee plan", zap.Error(err)) + return err + } + return nil +} + +func (p *plansStore) Get(ctx context.Context, planRef primitive.ObjectID) (*model.FeePlan, error) { + if planRef.IsZero() { + return nil, merrors.InvalidArgument("plansStore: zero plan reference") + } + result := &model.FeePlan{} + if err := p.repo.Get(ctx, planRef, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return nil, storage.ErrFeePlanNotFound + } + return nil, err + } + return result, nil +} + +func (p *plansStore) GetActivePlan(ctx context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) { + if orgRef.IsZero() { + return nil, merrors.InvalidArgument("plansStore: zero organization reference") + } + + limit := int64(1) + query := repository.Query(). + Filter(repository.OrgField(), orgRef). + Filter(repository.Field("active"), true). + Comparison(repository.Field("effectiveFrom"), builder.Lte, at). + Sort(repository.Field("effectiveFrom"), false). + Limit(&limit) + + query = query.And( + repository.Query().Or( + repository.Query().Filter(repository.Field("effectiveTo"), nil), + repository.Query().Comparison(repository.Field("effectiveTo"), builder.Gte, at), + ), + ) + + var plan *model.FeePlan + decoder := func(cursor *mongo.Cursor) error { + target := &model.FeePlan{} + if err := cursor.Decode(target); err != nil { + return err + } + plan = target + return nil + } + + if err := p.repo.FindManyByFilter(ctx, query, decoder); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return nil, storage.ErrFeePlanNotFound + } + return nil, err + } + + if plan == nil { + return nil, storage.ErrFeePlanNotFound + } + return plan, nil +} + +var _ storage.PlansStore = (*plansStore)(nil) diff --git a/api/billing/fees/storage/storage.go b/api/billing/fees/storage/storage.go new file mode 100644 index 0000000..9ae6451 --- /dev/null +++ b/api/billing/fees/storage/storage.go @@ -0,0 +1,36 @@ +package storage + +import ( + "context" + "time" + + "github.com/tech/sendico/billing/fees/storage/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type storageError string + +func (e storageError) Error() string { + return string(e) +} + +var ( + // ErrFeePlanNotFound indicates that a requested fee plan does not exist. + ErrFeePlanNotFound = storageError("billing.fees.storage: fee plan not found") + // ErrDuplicateFeePlan indicates that a unique plan constraint was violated. + ErrDuplicateFeePlan = storageError("billing.fees.storage: duplicate fee plan") +) + +// Repository defines the root storage contract for the fees service. +type Repository interface { + Ping(ctx context.Context) error + Plans() PlansStore +} + +// PlansStore exposes persistence operations for fee plans. +type PlansStore interface { + Create(ctx context.Context, plan *model.FeePlan) error + Update(ctx context.Context, plan *model.FeePlan) error + Get(ctx context.Context, planRef primitive.ObjectID) (*model.FeePlan, error) + GetActivePlan(ctx context.Context, orgRef primitive.ObjectID, at time.Time) (*model.FeePlan, error) +} diff --git a/api/chain/gateway/.air.toml b/api/chain/gateway/.air.toml new file mode 100644 index 0000000..cebf273 --- /dev/null +++ b/api/chain/gateway/.air.toml @@ -0,0 +1,32 @@ +# Config file for Air in TOML format + +root = "./../.." +tmp_dir = "tmp" + +[build] +cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/chain/gateway/internal/appversion.BuildDate=$(date)'\"" +bin = "./app" +full_bin = "./app --debug --config.file=config.yml" +include_ext = ["go", "yaml", "yml"] +exclude_dir = ["chain/gateway/tmp", "pkg/.git", "chain/gateway/env"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true +follow_symlink = true +log = "air.log" +delay = 0 +stop_on_error = true +send_interrupt = true +kill_delay = 500 +args_bin = [] + +[log] +time = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true diff --git a/api/chain/gateway/.gitignore b/api/chain/gateway/.gitignore new file mode 100644 index 0000000..c62beb6 --- /dev/null +++ b/api/chain/gateway/.gitignore @@ -0,0 +1,3 @@ +internal/generated +.gocache +app diff --git a/api/chain/gateway/client/client.go b/api/chain/gateway/client/client.go new file mode 100644 index 0000000..b54b044 --- /dev/null +++ b/api/chain/gateway/client/client.go @@ -0,0 +1,148 @@ +package client + +import ( + "context" + "crypto/tls" + "fmt" + "strings" + "time" + + "github.com/tech/sendico/pkg/merrors" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Client exposes typed helpers around the chain gateway gRPC API. +type Client interface { + CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) + GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) + ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) + GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) + SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) + GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) + ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) + EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) + Close() error +} + +type grpcGatewayClient interface { + CreateManagedWallet(ctx context.Context, in *gatewayv1.CreateManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.CreateManagedWalletResponse, error) + GetManagedWallet(ctx context.Context, in *gatewayv1.GetManagedWalletRequest, opts ...grpc.CallOption) (*gatewayv1.GetManagedWalletResponse, error) + ListManagedWallets(ctx context.Context, in *gatewayv1.ListManagedWalletsRequest, opts ...grpc.CallOption) (*gatewayv1.ListManagedWalletsResponse, error) + GetWalletBalance(ctx context.Context, in *gatewayv1.GetWalletBalanceRequest, opts ...grpc.CallOption) (*gatewayv1.GetWalletBalanceResponse, error) + SubmitTransfer(ctx context.Context, in *gatewayv1.SubmitTransferRequest, opts ...grpc.CallOption) (*gatewayv1.SubmitTransferResponse, error) + GetTransfer(ctx context.Context, in *gatewayv1.GetTransferRequest, opts ...grpc.CallOption) (*gatewayv1.GetTransferResponse, error) + ListTransfers(ctx context.Context, in *gatewayv1.ListTransfersRequest, opts ...grpc.CallOption) (*gatewayv1.ListTransfersResponse, error) + EstimateTransferFee(ctx context.Context, in *gatewayv1.EstimateTransferFeeRequest, opts ...grpc.CallOption) (*gatewayv1.EstimateTransferFeeResponse, error) +} + +type chainGatewayClient struct { + cfg Config + conn *grpc.ClientConn + client grpcGatewayClient +} + +// New dials the chain gateway endpoint and returns a ready client. +func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { + cfg.setDefaults() + if strings.TrimSpace(cfg.Address) == "" { + return nil, merrors.InvalidArgument("chain-gateway: address is required") + } + + dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) + defer cancel() + + dialOpts := make([]grpc.DialOption, 0, len(opts)+1) + dialOpts = append(dialOpts, opts...) + + if cfg.Insecure { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + } + + conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) + if err != nil { + return nil, merrors.Internal(fmt.Sprintf("chain-gateway: dial %s: %s", cfg.Address, err.Error())) + } + + return &chainGatewayClient{ + cfg: cfg, + conn: conn, + client: gatewayv1.NewChainGatewayServiceClient(conn), + }, nil +} + +// NewWithClient injects a pre-built gateway client (useful for tests). +func NewWithClient(cfg Config, gc grpcGatewayClient) Client { + cfg.setDefaults() + return &chainGatewayClient{ + cfg: cfg, + client: gc, + } +} + +func (c *chainGatewayClient) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +func (c *chainGatewayClient) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.CreateManagedWallet(ctx, req) +} + +func (c *chainGatewayClient) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetManagedWallet(ctx, req) +} + +func (c *chainGatewayClient) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ListManagedWallets(ctx, req) +} + +func (c *chainGatewayClient) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetWalletBalance(ctx, req) +} + +func (c *chainGatewayClient) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.SubmitTransfer(ctx, req) +} + +func (c *chainGatewayClient) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetTransfer(ctx, req) +} + +func (c *chainGatewayClient) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ListTransfers(ctx, req) +} + +func (c *chainGatewayClient) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.EstimateTransferFee(ctx, req) +} + +func (c *chainGatewayClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + return context.WithTimeout(ctx, timeout) +} diff --git a/api/chain/gateway/client/config.go b/api/chain/gateway/client/config.go new file mode 100644 index 0000000..b0ae55a --- /dev/null +++ b/api/chain/gateway/client/config.go @@ -0,0 +1,20 @@ +package client + +import "time" + +// Config captures connection settings for the chain gateway gRPC service. +type Config struct { + Address string + DialTimeout time.Duration + CallTimeout time.Duration + Insecure bool +} + +func (c *Config) setDefaults() { + if c.DialTimeout <= 0 { + c.DialTimeout = 5 * time.Second + } + if c.CallTimeout <= 0 { + c.CallTimeout = 3 * time.Second + } +} diff --git a/api/chain/gateway/client/fake.go b/api/chain/gateway/client/fake.go new file mode 100644 index 0000000..8974b51 --- /dev/null +++ b/api/chain/gateway/client/fake.go @@ -0,0 +1,83 @@ +package client + +import ( + "context" + + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" +) + +// Fake implements Client for tests. +type Fake struct { + CreateManagedWalletFn func(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) + GetManagedWalletFn func(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) + ListManagedWalletsFn func(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) + GetWalletBalanceFn func(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) + SubmitTransferFn func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) + GetTransferFn func(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) + ListTransfersFn func(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) + EstimateTransferFeeFn func(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) + CloseFn func() error +} + +func (f *Fake) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) { + if f.CreateManagedWalletFn != nil { + return f.CreateManagedWalletFn(ctx, req) + } + return &gatewayv1.CreateManagedWalletResponse{}, nil +} + +func (f *Fake) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) { + if f.GetManagedWalletFn != nil { + return f.GetManagedWalletFn(ctx, req) + } + return &gatewayv1.GetManagedWalletResponse{}, nil +} + +func (f *Fake) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) { + if f.ListManagedWalletsFn != nil { + return f.ListManagedWalletsFn(ctx, req) + } + return &gatewayv1.ListManagedWalletsResponse{}, nil +} + +func (f *Fake) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) { + if f.GetWalletBalanceFn != nil { + return f.GetWalletBalanceFn(ctx, req) + } + return &gatewayv1.GetWalletBalanceResponse{}, nil +} + +func (f *Fake) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) { + if f.SubmitTransferFn != nil { + return f.SubmitTransferFn(ctx, req) + } + return &gatewayv1.SubmitTransferResponse{}, nil +} + +func (f *Fake) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) { + if f.GetTransferFn != nil { + return f.GetTransferFn(ctx, req) + } + return &gatewayv1.GetTransferResponse{}, nil +} + +func (f *Fake) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) { + if f.ListTransfersFn != nil { + return f.ListTransfersFn(ctx, req) + } + return &gatewayv1.ListTransfersResponse{}, nil +} + +func (f *Fake) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) { + if f.EstimateTransferFeeFn != nil { + return f.EstimateTransferFeeFn(ctx, req) + } + return &gatewayv1.EstimateTransferFeeResponse{}, nil +} + +func (f *Fake) Close() error { + if f.CloseFn != nil { + return f.CloseFn() + } + return nil +} diff --git a/api/chain/gateway/config.yml b/api/chain/gateway/config.yml new file mode 100644 index 0000000..33d08bf --- /dev/null +++ b/api/chain/gateway/config.yml @@ -0,0 +1,57 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50070" + enable_reflection: true + enable_health: true + +metrics: + address: ":9403" + +database: + driver: mongodb + settings: + host_env: CHAIN_GATEWAY_MONGO_HOST + port_env: CHAIN_GATEWAY_MONGO_PORT + database_env: CHAIN_GATEWAY_MONGO_DATABASE + user_env: CHAIN_GATEWAY_MONGO_USER + password_env: CHAIN_GATEWAY_MONGO_PASSWORD + auth_source_env: CHAIN_GATEWAY_MONGO_AUTH_SOURCE + replica_set_env: CHAIN_GATEWAY_MONGO_REPLICA_SET + +messaging: + driver: NATS + settings: + url_env: NATS_URL + host_env: NATS_HOST + port_env: NATS_PORT + username_env: NATS_USER + password_env: NATS_PASSWORD + broker_name: Chain Gateway Service + max_reconnects: 10 + reconnect_wait: 5 + +chains: + - name: arbitrum_one + rpc_url_env: CHAIN_GATEWAY_ARBITRUM_RPC_URL + tokens: + - symbol: USDC + contract: "0xaf88d065e77c8cc2239327c5edb3a432268e5831" + - symbol: USDT + contract: "0xfd086bc7cd5c481dcc9c85ebe478a1c0b69fcbb9" + +service_wallet: + chain: arbitrum_one + address: "0xSERVICE_WALLET_ADDRESS" + private_key_env: CHAIN_GATEWAY_SERVICE_WALLET_KEY + +key_management: + driver: vault + settings: + address: "http://vault:8200" + token_env: CHAIN_GATEWAY_VAULT_TOKEN + namespace: "" + mount_path: secret + key_prefix: chain/gateway/wallets diff --git a/api/chain/gateway/go.mod b/api/chain/gateway/go.mod new file mode 100644 index 0000000..d6e11b5 --- /dev/null +++ b/api/chain/gateway/go.mod @@ -0,0 +1,90 @@ +module github.com/tech/sendico/chain/gateway + +go 1.25.3 + +replace github.com/tech/sendico/pkg => ../../pkg + +require ( + github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 + github.com/ethereum/go-ethereum v1.16.7 + github.com/hashicorp/vault/api v1.22.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/stretchr/testify v1.11.1 + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver v1.17.6 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/Microsoft/go-winio v0.6.2 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11 // indirect + github.com/beorn7/perks v1.0.1 // indirect + github.com/bits-and-blooms/bitset v1.24.3 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/cenkalti/backoff/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/consensys/gnark-crypto v0.19.2 // indirect + github.com/crate-crypto/go-eth-kzg v1.4.0 // indirect + github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/deckarep/golang-set/v2 v2.8.0 // indirect + github.com/ethereum/c-kzg-4844/v2 v2.1.5 // indirect + github.com/ethereum/go-verkle v0.2.2 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/go-jose/go-jose/v4 v4.1.3 // indirect + github.com/go-ole/go-ole v1.3.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/gorilla/websocket v1.5.3 // indirect + github.com/hashicorp/errwrap v1.1.0 // 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/holiman/uint256 v1.3.2 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/go-homedir v1.1.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/ryanuber/go-glob v1.0.0 // indirect + github.com/shirou/gopsutil v3.21.11+incompatible // indirect + github.com/supranational/blst v0.3.16 // indirect + github.com/tklauser/go-sysconf v0.3.15 // indirect + github.com/tklauser/numcpus v0.10.0 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + github.com/yusufpapurcu/wmi v1.2.4 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + golang.org/x/time v0.14.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect +) diff --git a/api/chain/gateway/go.sum b/api/chain/gateway/go.sum new file mode 100644 index 0000000..0f2079d --- /dev/null +++ b/api/chain/gateway/go.sum @@ -0,0 +1,379 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161/go.mod h1:xomTg63KZ2rFqZQzSB4Vz2SUXa1BpHTVz9L5PTmPC4E= +github.com/DataDog/zstd v1.4.5 h1:EndNeuB0l9syBZhut0wns3gV1hL8zX8LIu6ZiVHWLIQ= +github.com/DataDog/zstd v1.4.5/go.mod h1:1jcaCB/ufaK+sKp1NBhlGmpz41jOoPQ35bpF36t7BBo= +github.com/Microsoft/go-winio v0.6.2 h1:F2VQgta7ecxGYO8k3ZZz3RS8fVIXVxONVUPlNERoyfY= +github.com/Microsoft/go-winio v0.6.2/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11 h1:cP8UbFCldZ6uVbZnI3/EI4FSdO9NaYnx4hY+tyW6FbU= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20251106012722-c7be33e82a11/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/VictoriaMetrics/fastcache v1.13.0 h1:AW4mheMR5Vd9FkAPUv+NH6Nhw+fmbTMGMsNAoA/+4G0= +github.com/VictoriaMetrics/fastcache v1.13.0/go.mod h1:hHXhl4DA2fTL2HTZDJFXWgW0LNjo6B+4aj2Wmng3TjU= +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/bits-and-blooms/bitset v1.24.3 h1:Bte86SlO3lwPQqww+7BE9ZuUCKIjfqnG5jtEyqA9y9Y= +github.com/bits-and-blooms/bitset v1.24.3/go.mod h1:7hO7Gc7Pp1vODcmWvKMRA9BNmbv6a/7QIWpPxHddWR8= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= +github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs= +github.com/cockroachdb/errors v1.11.3 h1:5bA+k2Y6r+oz/6Z/RFlNeVCesGARKuC6YymtcDrbC/I= +github.com/cockroachdb/errors v1.11.3/go.mod h1:m4UIW4CDjx+R5cybPsNrRbreomiFqt8o1h1wUVazSd8= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce h1:giXvy4KSc/6g/esnpM7Geqxka4WSqI1SZc7sMJFd3y4= +github.com/cockroachdb/fifo v0.0.0-20240606204812-0bbfbd93a7ce/go.mod h1:9/y3cnZ5GKakj/H4y9r9GTjCvAFta7KLgSHPJJYc52M= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b h1:r6VH0faHjZeQy818SGhaone5OnYfxFR/+AzdY3sf5aE= +github.com/cockroachdb/logtags v0.0.0-20230118201751-21c54148d20b/go.mod h1:Vz9DsVWQQhf3vs21MhPMZpMGSht7O/2vFW2xusFUVOs= +github.com/cockroachdb/pebble v1.1.5 h1:5AAWCBWbat0uE0blr8qzufZP5tBjkRyy/jWe1QWLnvw= +github.com/cockroachdb/pebble v1.1.5/go.mod h1:17wO9el1YEigxkP/YtV8NtCivQDgoCyBg5c4VR/eOWo= +github.com/cockroachdb/redact v1.1.5 h1:u1PMllDkdFfPWaNGMyLD1+so+aq3uUItthCFqzwPJ30= +github.com/cockroachdb/redact v1.1.5/go.mod h1:BVNblN9mBWFyMyqK1k3AAiSxhvhfK2oOZZ2lK+dpvRg= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06 h1:zuQyyAKVxetITBuuhv3BI9cMrmStnpT18zmgmTxunpo= +github.com/cockroachdb/tokenbucket v0.0.0-20230807174530-cc333fc44b06/go.mod h1:7nc4anLGjupUW/PeY5qiNYsdNXj7zopG+eqsS7To5IQ= +github.com/consensys/gnark-crypto v0.19.2 h1:qrEAIXq3T4egxqiliFFoNrepkIWVEeIYwt3UL0fvS80= +github.com/consensys/gnark-crypto v0.19.2/go.mod h1:rT23F0XSZqE0mUA0+pRtnL56IbPxs6gp4CeRsBk4XS0= +github.com/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +github.com/cpuguy83/go-md2man/v2 v2.0.5 h1:ZtcqGrnekaHpVLArFSe4HK5DoKx1T0rq2DwVB0alcyc= +github.com/cpuguy83/go-md2man/v2 v2.0.5/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o= +github.com/crate-crypto/go-eth-kzg v1.4.0 h1:WzDGjHk4gFg6YzV0rJOAsTK4z3Qkz5jd4RE3DAvPFkg= +github.com/crate-crypto/go-eth-kzg v1.4.0/go.mod h1:J9/u5sWfznSObptgfa92Jq8rTswn6ahQWEuiLHOjCUI= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a h1:W8mUrRp6NOVl3J+MYp5kPMoUZPp7aOYHtaua31lwRHg= +github.com/crate-crypto/go-ipa v0.0.0-20240724233137-53bbb0ceb27a/go.mod h1:sTwzHBvIzm2RfVCGNEBZgRyjwK40bVoun3ZnGOCafNM= +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/dchest/siphash v1.2.3 h1:QXwFc8cFOR2dSa/gE6o/HokBMWtLUaNDVd+22aKHeEA= +github.com/dchest/siphash v1.2.3/go.mod h1:0NvQU092bT0ipiFN++/rXm69QG9tVxLAlQHIXMPAkHc= +github.com/deckarep/golang-set/v2 v2.8.0 h1:swm0rlPCmdWn9mESxKOjWk8hXSqoxOp+ZlfuyaAdFlQ= +github.com/deckarep/golang-set/v2 v2.8.0/go.mod h1:VAky9rY/yGXJOLEDv3OMci+7wtDpOF4IN+y82NBOac4= +github.com/decred/dcrd/crypto/blake256 v1.1.0 h1:zPMNGQCm0g4QTY27fOCorQW7EryeQ/U0x++OzVrdms8= +github.com/decred/dcrd/crypto/blake256 v1.1.0/go.mod h1:2OfgNZ5wDpcsFmHmCK5gZTPcCXqlm2ArzUIkw9czNJo= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0 h1:NMZiJj8QnKe1LgsbDayM4UoHwbvwDRwnI3hwNaAHRnc= +github.com/decred/dcrd/dcrec/secp256k1/v4 v4.4.0/go.mod h1:ZXNYxsqcloTdSy/rNShjYzMhyjf0LaoftYK0p+A3h40= +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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/emicklei/dot v1.6.2 h1:08GN+DD79cy/tzN6uLCT84+2Wk9u+wvqP+Hkx/dIR8A= +github.com/emicklei/dot v1.6.2/go.mod h1:DeV7GvQtIw4h2u73RKBkkFdvVAz0D9fzeJrgPW6gy/s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5 h1:aVtoLK5xwJ6c5RiqO8g8ptJ5KU+2Hdquf6G3aXiHh5s= +github.com/ethereum/c-kzg-4844/v2 v2.1.5/go.mod h1:u59hRTTah4Co6i9fDWtiCjTrblJv0UwsqZKCc0GfgUs= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab h1:rvv6MJhy07IMfEKuARQ9TKojGqLVNxQajaXEp/BoqSk= +github.com/ethereum/go-bigmodexpfix v0.0.0-20250911101455-f9e208c548ab/go.mod h1:IuLm4IsPipXKF7CW5Lzf68PIbZ5yl7FFd74l/E0o9A8= +github.com/ethereum/go-ethereum v1.16.7 h1:qeM4TvbrWK0UC0tgkZ7NiRsmBGwsjqc64BHo20U59UQ= +github.com/ethereum/go-ethereum v1.16.7/go.mod h1:Fs6QebQbavneQTYcA39PEKv2+zIjX7rPUZ14DER46wk= +github.com/ethereum/go-verkle v0.2.2 h1:I2W0WjnrFUIzzVPwm8ykY+7pL2d4VhlsePn4j7cnFk8= +github.com/ethereum/go-verkle v0.2.2/go.mod h1:M3b90YRnzqKyyzBEWJGqj8Qff4IDeXnzFw0P9bFw3uk= +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/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/ferranbt/fastssz v0.1.4 h1:OCDB+dYDEQDvAgtAGnTSidK1Pe2tW3nFV40XyMkTeDY= +github.com/ferranbt/fastssz v0.1.4/go.mod h1:Ea3+oeoRGGLGm5shYAeDgu6PGUlcvQhE2fILyD9+tGg= +github.com/getsentry/sentry-go v0.27.0 h1:Pv98CIbtB3LkMWmXi4Joa5OOcwbmnX88sF5qbK3r3Ps= +github.com/getsentry/sentry-go v0.27.0/go.mod h1:lc76E2QywIyW8WuBnwl8Lc4bkmQH4+w1gwTf25trprY= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +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/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/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/gofrs/flock v0.12.1 h1:MTLVXXHf8ekldpJk3AKicLij9MdwOWkZ+a/jHHZby9E= +github.com/gofrs/flock v0.12.1/go.mod h1:9zxTsyu5xtJ9DK+1tFZyibEV7y3uwDxPPfbxeeHCoD0= +github.com/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang-jwt/jwt/v4 v4.5.2 h1:YtQM7lnr8iZ+j5q71MGKkNw9Mn7AjHM68uc9g5fXeUI= +github.com/golang-jwt/jwt/v4 v4.5.2/go.mod h1:m21LjoU+eqJr34lmDMbreY2eSTRJ1cv77w39/MY0Ch0= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0= +github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= +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/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg= +github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE= +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-bexpr v0.1.10 h1:9kuI5PFotCboP3dkDYFr/wi0gg0QVbSNz5oFRpxn4uE= +github.com/hashicorp/go-bexpr v0.1.10/go.mod h1:oxlubA2vC/gFVfX1A6JGp7ls7uCDlfJn732ehYYg+g0= +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/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db h1:IZUYC/xb3giYwBLMnr8d0TGTzPKFGNTCGgGLoyeX330= +github.com/holiman/billy v0.0.0-20250707135307-f2f9b9aae7db/go.mod h1:xTEYN9KCHxuYHs+NmrmzFcnvHMzLLNiGFafCb1n3Mfg= +github.com/holiman/bloomfilter/v2 v2.0.3 h1:73e0e/V0tCydx14a0SCYS/EWCxgwLZ18CZcZKVu0fao= +github.com/holiman/bloomfilter/v2 v2.0.3/go.mod h1:zpoh+gs7qcpqrHr3dB55AMiJwo0iURXE7ZOP9L9hSkA= +github.com/holiman/uint256 v1.3.2 h1:a9EgMPSC1AAaj1SZL5zIQD3WbwTuHrMGOerLjGmM/TA= +github.com/holiman/uint256 v1.3.2/go.mod h1:EOMSn4q6Nyt9P6efbI3bueV4e1b3dGlUCXeiRV4ng7E= +github.com/huin/goupnp v1.3.0 h1:UvLUlWDNpoUdYzb2TCn+MuTWtcjXKSza2n6CBdQ0xXc= +github.com/huin/goupnp v1.3.0/go.mod h1:gnGPsThkYa7bFi/KWmEysQRf48l2dvR5bxr2OFckNX8= +github.com/jackpal/go-nat-pmp v1.0.2 h1:KzKSgb7qkJvOUTqYl9/Hg/me3pWgBmERKrTGD7BdWus= +github.com/jackpal/go-nat-pmp v1.0.2/go.mod h1:QPH045xvCAeXUZOxsnwmrtiCoxIr9eob+4orBN1SBKc= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/klauspost/cpuid/v2 v2.0.9 h1:lgaqFMSdTdQYdZ04uHyN2d/eKdOMyi2YLSvlQIBFYa4= +github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +github.com/leanovate/gopter v0.2.11 h1:vRjThO1EKPb/1NsDXuDrzldR28RLkBflWYcU9CvzWu4= +github.com/leanovate/gopter v0.2.11/go.mod h1:aK3tzZP/C+p1m3SPRE4SYZFGP7jjkuSI4f7Xvpt0S9c= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +github.com/mattn/go-runewidth v0.0.13 h1:lTGmDsbAYt5DmK6OnoV7EuIF1wEIFAcxld6ypU4OSgU= +github.com/mattn/go-runewidth v0.0.13/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w= +github.com/minio/sha256-simd v1.0.0 h1:v1ta+49hkWZyvaKwrQB8elexRqm6Y0aMLjCNsrYxo6g= +github.com/minio/sha256-simd v1.0.0/go.mod h1:OuYzVNI5vcoYIAmbIvHPl3N3jUzVedXbKy5RFepssQM= +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/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo= +github.com/mitchellh/pointerstructure v1.2.0 h1:O+i9nHnXS3l/9Wu7r4NrEdwA2VFTicjUEN1uBnDo34A= +github.com/mitchellh/pointerstructure v1.2.0/go.mod h1:BRAsLI5zgXmw97Lf6s25bs8ohIXc3tViBH44KcwB2g4= +github.com/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/olekukonko/tablewriter v0.0.5 h1:P2Ga83D34wi1o9J6Wh1mRuqd4mF/x/lgBS7N7AbDhec= +github.com/olekukonko/tablewriter v0.0.5/go.mod h1:hPp6KlRPjbx+hW8ykQs1w3UBbZlj6HuIJcUGPhkA7kY= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= +github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= +github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= +github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= +github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= +github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= +github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= +github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= +github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM= +github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY= +github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc= +github.com/rogpeppe/go-internal v1.12.0 h1:exVL4IDcn6na9z1rAb56Vxr+CgyK3nn3O+epU5NdKM8= +github.com/rogpeppe/go-internal v1.12.0/go.mod h1:E+RYuTGaKKdloAfM02xzb0FW3Paa99yedzYV+kq4uf4= +github.com/rs/cors v1.7.0 h1:+88SsELBHx5r+hZ8TCkggzSstaWNbDvThkVK8H6f9ik= +github.com/rs/cors v1.7.0/go.mod h1:gFx+x8UowdsKA9AchylcLynDq+nNFfI8FkUZdN/jGCU= +github.com/russross/blackfriday/v2 v2.1.0 h1:JIOH55/0cWyOuilr9/qlrm0BSXldqnqwMsf35Ld67mk= +github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM= +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/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil v3.21.11+incompatible/go.mod h1:5b4v6he4MtMOwMlS0TUMTu2PcXUg8+E1lC7eC3UO/RA= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/supranational/blst v0.3.16 h1:bTDadT+3fK497EvLdWRQEjiGnUtzJ7jjIUMF0jqwYhE= +github.com/supranational/blst v0.3.16/go.mod h1:jZJtfjgudtNl4en1tzwPIV3KjUnQUvG3/j+w+fVonLw= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7 h1:epCh84lMvA70Z7CTTCmYQn2CKbY8j86K7/FAIr141uY= +github.com/syndtr/goleveldb v1.0.1-0.20210819022825-2ae1ddf74ef7/go.mod h1:q4W45IWZaF22tdD+VEXcAWRA037jwmWEB5VWYORlTpc= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +github.com/urfave/cli/v2 v2.27.5 h1:WoHEJLdsXr6dDWoJgMq/CboDmyY/8HMMH1fTECbih+w= +github.com/urfave/cli/v2 v2.27.5/go.mod h1:3Sevf16NykTbInEnD0yKkjDAeZDS0A6bzhBH5hrMvTQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1 h1:gEOO8jv9F4OT7lGCjxCBTO/36wtF6j2nSip77qHd4x4= +github.com/xrash/smetrics v0.0.0-20240521201337-686a1a2994c1/go.mod h1:Ohn+xnUBiLI6FVj/9LpzZWtj1/D6lUovWYBkxHVV3aM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546 h1:mgKeJMpvi0yx/sU5GsxQ7p6s2wtOnGAHZWCHUM4KGzY= +golang.org/x/exp v0.0.0-20251023183803-a4bb9ffd2546/go.mod h1:j/pmGrbnkbPtQfxEe5D0VQhZC6qKbfKifgD0oM7sR70= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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/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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= +gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= +gopkg.in/yaml.v2 v2.4.0 h1:D8xgwECY7CYvx+Y2n4sBz93Jn9JRvxdiyyo8CTfuKaY= +gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/chain/gateway/internal/appversion/version.go b/api/chain/gateway/internal/appversion/version.go new file mode 100644 index 0000000..d159c12 --- /dev/null +++ b/api/chain/gateway/internal/appversion/version.go @@ -0,0 +1,27 @@ +package appversion + +import ( + "github.com/tech/sendico/pkg/version" + vf "github.com/tech/sendico/pkg/version/factory" +) + +// Build information. Populated at build-time. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +func Create() version.Printer { + info := version.Info{ + Program: "MeetX Connectica Chain Gateway Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&info) +} diff --git a/api/chain/gateway/internal/keymanager/config.go b/api/chain/gateway/internal/keymanager/config.go new file mode 100644 index 0000000..ceb80b0 --- /dev/null +++ b/api/chain/gateway/internal/keymanager/config.go @@ -0,0 +1,13 @@ +package keymanager + +import "github.com/tech/sendico/pkg/model" + +// Driver identifies the key management backend implementation. +type Driver string + +const ( + DriverVault Driver = "vault" +) + +// Config represents a configured key manager driver with arbitrary settings. +type Config = model.DriverConfig[Driver] diff --git a/api/chain/gateway/internal/keymanager/keymanager.go b/api/chain/gateway/internal/keymanager/keymanager.go new file mode 100644 index 0000000..9b20c9c --- /dev/null +++ b/api/chain/gateway/internal/keymanager/keymanager.go @@ -0,0 +1,23 @@ +package keymanager + +import ( + "context" + "math/big" + + "github.com/ethereum/go-ethereum/core/types" +) + +// ManagedWalletKey captures information returned after provisioning a managed wallet key. +type ManagedWalletKey struct { + KeyID string + Address string + PublicKey string +} + +// Manager defines the contract for managing managed wallet keys. +type Manager interface { + // CreateManagedWalletKey provisions a new managed wallet key for the provided wallet reference and network. + CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*ManagedWalletKey, error) + // SignTransaction signs the provided transaction using the identified key material. + SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) +} diff --git a/api/chain/gateway/internal/keymanager/vault/manager.go b/api/chain/gateway/internal/keymanager/vault/manager.go new file mode 100644 index 0000000..85f9b37 --- /dev/null +++ b/api/chain/gateway/internal/keymanager/vault/manager.go @@ -0,0 +1,269 @@ +package vault + +import ( + "context" + "crypto/ecdsa" + "crypto/rand" + "encoding/hex" + "math/big" + "os" + "path" + "strings" + + "github.com/decred/dcrd/dcrec/secp256k1/v4" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/crypto" + "github.com/hashicorp/vault/api" + "go.uber.org/zap" + + "github.com/tech/sendico/chain/gateway/internal/keymanager" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" +) + +// Config describes how to connect to Vault for managed wallet keys. +type Config struct { + Address string `mapstructure:"address"` + TokenEnv string `mapstructure:"token_env"` + Namespace string `mapstructure:"namespace"` + MountPath string `mapstructure:"mount_path"` + KeyPrefix string `mapstructure:"key_prefix"` +} + +// Manager implements the keymanager.Manager contract backed by HashiCorp Vault. +type Manager struct { + logger mlogger.Logger + client *api.Client + store *api.KVv2 + keyPrefix string +} + +// New constructs a Vault-backed key manager. +func New(logger mlogger.Logger, cfg Config) (*Manager, error) { + if logger == nil { + return nil, merrors.InvalidArgument("vault key manager: logger is required") + } + address := strings.TrimSpace(cfg.Address) + if address == "" { + logger.Error("vault address missing") + return nil, merrors.InvalidArgument("vault key manager: address is required") + } + tokenEnv := strings.TrimSpace(cfg.TokenEnv) + if tokenEnv == "" { + logger.Error("vault token env missing") + return nil, merrors.InvalidArgument("vault key manager: token_env is required") + } + token := strings.TrimSpace(os.Getenv(tokenEnv)) + if token == "" { + logger.Error("vault token env not set", zap.String("env", tokenEnv)) + return nil, merrors.InvalidArgument("vault key manager: token env " + tokenEnv + " is not set") + } + mountPath := strings.Trim(strings.TrimSpace(cfg.MountPath), "/") + if mountPath == "" { + logger.Error("vault mount path missing") + return nil, merrors.InvalidArgument("vault key manager: mount_path is required") + } + keyPrefix := strings.Trim(strings.TrimSpace(cfg.KeyPrefix), "/") + if keyPrefix == "" { + keyPrefix = "chain/gateway/wallets" + } + + clientCfg := api.DefaultConfig() + clientCfg.Address = address + + client, err := api.NewClient(clientCfg) + if err != nil { + logger.Error("failed to create vault client", zap.Error(err)) + return nil, merrors.Internal("vault key manager: failed to create client: " + err.Error()) + } + client.SetToken(token) + if ns := strings.TrimSpace(cfg.Namespace); ns != "" { + client.SetNamespace(ns) + } + + kv := client.KVv2(mountPath) + + return &Manager{ + logger: logger.Named("vault"), + client: client, + store: kv, + keyPrefix: keyPrefix, + }, nil +} + +// CreateManagedWalletKey creates a new managed wallet key and stores it in Vault. +func (m *Manager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) { + if strings.TrimSpace(walletRef) == "" { + m.logger.Warn("walletRef missing for managed key creation", zap.String("network", network)) + return nil, merrors.InvalidArgument("vault key manager: walletRef is required") + } + if strings.TrimSpace(network) == "" { + m.logger.Warn("network missing for managed key creation", zap.String("wallet_ref", walletRef)) + return nil, merrors.InvalidArgument("vault key manager: network is required") + } + + privateKey, err := ecdsa.GenerateKey(secp256k1.S256(), rand.Reader) + if err != nil { + m.logger.Warn("failed to generate managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err)) + return nil, merrors.Internal("vault key manager: failed to generate key: " + err.Error()) + } + privateKeyBytes := crypto.FromECDSA(privateKey) + publicKey := privateKey.PublicKey + publicKeyBytes := crypto.FromECDSAPub(&publicKey) + publicKeyHex := hex.EncodeToString(publicKeyBytes) + address := crypto.PubkeyToAddress(publicKey).Hex() + + err = m.persistKey(ctx, walletRef, network, privateKeyBytes, publicKeyBytes, address) + if err != nil { + m.logger.Warn("failed to persist managed wallet key", zap.String("wallet_ref", walletRef), zap.String("network", network), zap.Error(err)) + zeroBytes(privateKeyBytes) + zeroBytes(publicKeyBytes) + return nil, err + } + zeroBytes(privateKeyBytes) + zeroBytes(publicKeyBytes) + + m.logger.Info("managed wallet key created", + zap.String("wallet_ref", walletRef), + zap.String("network", network), + zap.String("address", strings.ToLower(address)), + ) + + return &keymanager.ManagedWalletKey{ + KeyID: m.buildKeyID(network, walletRef), + Address: strings.ToLower(address), + PublicKey: publicKeyHex, + }, nil +} + +func (m *Manager) persistKey(ctx context.Context, walletRef, network string, privateKey, publicKey []byte, address string) error { + secretPath := m.buildKeyID(network, walletRef) + payload := map[string]interface{}{ + "private_key": hex.EncodeToString(privateKey), + "public_key": hex.EncodeToString(publicKey), + "address": strings.ToLower(address), + "network": strings.ToLower(network), + } + if _, err := m.store.Put(ctx, secretPath, payload); err != nil { + return merrors.Internal("vault key manager: failed to write secret at " + secretPath + ": " + err.Error()) + } + return nil +} + +func (m *Manager) buildKeyID(network, walletRef string) string { + net := strings.Trim(strings.ToLower(network), "/") + return path.Join(m.keyPrefix, net, walletRef) +} + +// SignTransaction loads the key material from Vault and signs the transaction. +func (m *Manager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + if strings.TrimSpace(keyID) == "" { + m.logger.Warn("signing failed: empty key id") + return nil, merrors.InvalidArgument("vault key manager: keyID is required") + } + if tx == nil { + m.logger.Warn("signing failed: nil transaction", zap.String("key_id", keyID)) + return nil, merrors.InvalidArgument("vault key manager: transaction is nil") + } + if chainID == nil { + m.logger.Warn("signing failed: nil chain id", zap.String("key_id", keyID)) + return nil, merrors.InvalidArgument("vault key manager: chainID is nil") + } + + material, err := m.loadKey(ctx, keyID) + if err != nil { + m.logger.Warn("failed to load key material", zap.String("key_id", keyID), zap.Error(err)) + return nil, err + } + + keyBytes, err := hex.DecodeString(material.PrivateKey) + if err != nil { + m.logger.Warn("invalid key material", zap.String("key_id", keyID), zap.Error(err)) + return nil, merrors.Internal("vault key manager: invalid key material: " + err.Error()) + } + defer zeroBytes(keyBytes) + + privateKey, err := crypto.ToECDSA(keyBytes) + if err != nil { + m.logger.Warn("failed to construct private key", zap.String("key_id", keyID), zap.Error(err)) + return nil, merrors.Internal("vault key manager: failed to construct private key: " + err.Error()) + } + + signed, err := types.SignTx(tx, types.LatestSignerForChainID(chainID), privateKey) + if err != nil { + m.logger.Warn("failed to sign transaction", zap.String("key_id", keyID), zap.Error(err)) + return nil, merrors.Internal("vault key manager: failed to sign transaction: " + err.Error()) + } + m.logger.Info("transaction signed with managed key", + zap.String("key_id", keyID), + zap.String("network", material.Network), + zap.String("tx_hash", signed.Hash().Hex()), + ) + return signed, nil +} + +type keyMaterial struct { + PrivateKey string + PublicKey string + Address string + Network string +} + +func (m *Manager) loadKey(ctx context.Context, keyID string) (*keyMaterial, error) { + secretPath := strings.Trim(strings.TrimPrefix(keyID, "/"), "/") + secret, err := m.store.Get(ctx, secretPath) + if err != nil { + m.logger.Warn("failed to read secret", zap.String("path", secretPath), zap.Error(err)) + return nil, merrors.Internal("vault key manager: failed to read secret at " + secretPath + ": " + err.Error()) + } + if secret == nil || secret.Data == nil { + m.logger.Warn("secret not found", zap.String("path", secretPath)) + return nil, merrors.NoData("vault key manager: secret " + secretPath + " not found") + } + + getString := func(key string) (string, error) { + val, ok := secret.Data[key] + if !ok { + m.logger.Warn("secret missing field", zap.String("path", secretPath), zap.String("field", key)) + return "", merrors.Internal("vault key manager: secret " + secretPath + " missing " + key) + } + str, ok := val.(string) + if !ok || strings.TrimSpace(str) == "" { + m.logger.Warn("secret field invalid", zap.String("path", secretPath), zap.String("field", key)) + return "", merrors.Internal("vault key manager: secret " + secretPath + " invalid " + key) + } + return str, nil + } + + privateKey, err := getString("private_key") + if err != nil { + return nil, err + } + publicKey, err := getString("public_key") + if err != nil { + return nil, err + } + address, err := getString("address") + if err != nil { + return nil, err + } + network, err := getString("network") + if err != nil { + return nil, err + } + + return &keyMaterial{ + PrivateKey: privateKey, + PublicKey: publicKey, + Address: address, + Network: network, + }, nil +} + +func zeroBytes(data []byte) { + for i := range data { + data[i] = 0 + } +} + +var _ keymanager.Manager = (*Manager)(nil) diff --git a/api/chain/gateway/internal/server/internal/serverimp.go b/api/chain/gateway/internal/server/internal/serverimp.go new file mode 100644 index 0000000..a1cb7d8 --- /dev/null +++ b/api/chain/gateway/internal/server/internal/serverimp.go @@ -0,0 +1,259 @@ +package serverimp + +import ( + "context" + "os" + "strings" + "time" + + "github.com/mitchellh/mapstructure" + "github.com/tech/sendico/chain/gateway/internal/keymanager" + vaultmanager "github.com/tech/sendico/chain/gateway/internal/keymanager/vault" + gatewayservice "github.com/tech/sendico/chain/gateway/internal/service/gateway" + "github.com/tech/sendico/chain/gateway/storage" + gatewaymongo "github.com/tech/sendico/chain/gateway/storage/mongo" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/merrors" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type Imp struct { + logger mlogger.Logger + file string + debug bool + + config *config + app *grpcapp.App[storage.Repository] +} + +type config struct { + *grpcapp.Config `yaml:",inline"` + Chains []chainConfig `yaml:"chains"` + ServiceWallet serviceWalletConfig `yaml:"service_wallet"` + KeyManagement keymanager.Config `yaml:"key_management"` +} + +type chainConfig struct { + Name string `yaml:"name"` + RPCURLEnv string `yaml:"rpc_url_env"` + ChainID uint64 `yaml:"chain_id"` + NativeToken string `yaml:"native_token"` + Tokens []tokenConfig `yaml:"tokens"` +} + +type serviceWalletConfig struct { + Chain string `yaml:"chain"` + Address string `yaml:"address"` + AddressEnv string `yaml:"address_env"` + PrivateKeyEnv string `yaml:"private_key_env"` +} + +type tokenConfig struct { + Symbol string `yaml:"symbol"` + Contract string `yaml:"contract"` + ContractEnv string `yaml:"contract_env"` +} + +// Create initialises the chain gateway server implementation. +func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { + return &Imp{ + logger: logger.Named("server"), + file: file, + debug: debug, + }, nil +} + +func (i *Imp) Shutdown() { + if i.app == nil { + return + } + + timeout := 15 * time.Second + if i.config != nil && i.config.Runtime != nil { + timeout = i.config.Runtime.ShutdownTimeout() + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + i.app.Shutdown(ctx) +} + +func (i *Imp) Start() error { + cfg, err := i.loadConfig() + if err != nil { + return err + } + i.config = cfg + + repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { + return gatewaymongo.New(logger, conn) + } + + cl := i.logger.Named("config") + networkConfigs := resolveNetworkConfigs(cl.Named("network"), cfg.Chains) + walletConfig := resolveServiceWallet(cl.Named("wallet"), cfg.ServiceWallet) + keyManager, err := resolveKeyManager(i.logger.Named("key_manager"), cfg.KeyManagement) + if err != nil { + return err + } + + serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { + executor := gatewayservice.NewOnChainExecutor(logger, keyManager) + opts := []gatewayservice.Option{ + gatewayservice.WithNetworks(networkConfigs), + gatewayservice.WithServiceWallet(walletConfig), + gatewayservice.WithKeyManager(keyManager), + gatewayservice.WithTransferExecutor(executor), + } + return gatewayservice.NewService(logger, repo, producer, opts...), nil + } + + app, err := grpcapp.NewApp(i.logger, "chain_gateway", cfg.Config, i.debug, repoFactory, serviceFactory) + if err != nil { + return err + } + i.app = app + + return i.app.Start() +} + +func (i *Imp) loadConfig() (*config, error) { + data, err := os.ReadFile(i.file) + if err != nil { + i.logger.Error("could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) + return nil, err + } + + cfg := &config{ + Config: &grpcapp.Config{}, + } + if err := yaml.Unmarshal(data, cfg); err != nil { + i.logger.Error("failed to parse configuration", zap.Error(err)) + return nil, err + } + + if cfg.Runtime == nil { + cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} + } + + if cfg.GRPC == nil { + cfg.GRPC = &routers.GRPCConfig{ + Network: "tcp", + Address: ":50070", + EnableReflection: true, + EnableHealth: true, + } + } + + return cfg, nil +} + +func resolveNetworkConfigs(logger mlogger.Logger, chains []chainConfig) []gatewayservice.Network { + result := make([]gatewayservice.Network, 0, len(chains)) + for _, chain := range chains { + if strings.TrimSpace(chain.Name) == "" { + logger.Warn("skipping unnamed chain configuration") + continue + } + rpcURL := strings.TrimSpace(os.Getenv(chain.RPCURLEnv)) + if rpcURL == "" { + logger.Warn("chain RPC endpoint not configured", zap.String("chain", chain.Name), zap.String("env", chain.RPCURLEnv)) + } + contracts := make([]gatewayservice.TokenContract, 0, len(chain.Tokens)) + for _, token := range chain.Tokens { + symbol := strings.TrimSpace(token.Symbol) + if symbol == "" { + logger.Warn("skipping token with empty symbol", zap.String("chain", chain.Name)) + continue + } + addr := strings.TrimSpace(token.Contract) + env := strings.TrimSpace(token.ContractEnv) + if addr == "" && env != "" { + addr = strings.TrimSpace(os.Getenv(env)) + } + if addr == "" { + if env != "" { + logger.Warn("token contract not configured", zap.String("token", symbol), zap.String("env", env), zap.String("chain", chain.Name)) + } else { + logger.Warn("token contract not configured", zap.String("token", symbol), zap.String("chain", chain.Name)) + } + continue + } + contracts = append(contracts, gatewayservice.TokenContract{ + Symbol: symbol, + ContractAddress: addr, + }) + } + + result = append(result, gatewayservice.Network{ + Name: chain.Name, + RPCURL: rpcURL, + ChainID: chain.ChainID, + NativeToken: chain.NativeToken, + TokenConfigs: contracts, + }) + } + return result +} + +func resolveServiceWallet(logger mlogger.Logger, cfg serviceWalletConfig) gatewayservice.ServiceWallet { + address := strings.TrimSpace(cfg.Address) + if address == "" && cfg.AddressEnv != "" { + address = strings.TrimSpace(os.Getenv(cfg.AddressEnv)) + } + + privateKey := strings.TrimSpace(os.Getenv(cfg.PrivateKeyEnv)) + + if address == "" { + if cfg.AddressEnv != "" { + logger.Warn("service wallet address not configured", zap.String("env", cfg.AddressEnv)) + } else { + logger.Warn("service wallet address not configured", zap.String("chain", cfg.Chain)) + } + } + if privateKey == "" { + logger.Warn("service wallet private key not configured", zap.String("env", cfg.PrivateKeyEnv)) + } + + return gatewayservice.ServiceWallet{ + Network: cfg.Chain, + Address: address, + PrivateKey: privateKey, + } +} + +func resolveKeyManager(logger mlogger.Logger, cfg keymanager.Config) (keymanager.Manager, error) { + driver := strings.ToLower(strings.TrimSpace(string(cfg.Driver))) + if driver == "" { + err := merrors.InvalidArgument("key management driver is not configured") + logger.Error("key management driver missing") + return nil, err + } + + switch keymanager.Driver(driver) { + case keymanager.DriverVault: + settings := vaultmanager.Config{} + if len(cfg.Settings) > 0 { + if err := mapstructure.Decode(cfg.Settings, &settings); err != nil { + logger.Error("failed to decode vault key manager settings", zap.Error(err), zap.Any("settings", cfg.Settings)) + return nil, merrors.InvalidArgument("invalid vault key manager settings: " + err.Error()) + } + } + manager, err := vaultmanager.New(logger, settings) + if err != nil { + logger.Error("failed to initialise vault key manager", zap.Error(err)) + return nil, err + } + return manager, nil + default: + err := merrors.InvalidArgument("unsupported key management driver: " + driver) + logger.Error("unsupported key management driver", zap.String("driver", driver)) + return nil, err + } +} diff --git a/api/chain/gateway/internal/server/server.go b/api/chain/gateway/internal/server/server.go new file mode 100644 index 0000000..c058db1 --- /dev/null +++ b/api/chain/gateway/internal/server/server.go @@ -0,0 +1,12 @@ +package server + +import ( + serverimp "github.com/tech/sendico/chain/gateway/internal/server/internal" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" +) + +// Create constructs the chain gateway server implementation. +func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return serverimp.Create(logger, file, debug) +} diff --git a/api/chain/gateway/internal/service/gateway/conversion_helpers.go b/api/chain/gateway/internal/service/gateway/conversion_helpers.go new file mode 100644 index 0000000..a1803d7 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/conversion_helpers.go @@ -0,0 +1,21 @@ +package gateway + +import moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + +func cloneMoney(m *moneyv1.Money) *moneyv1.Money { + if m == nil { + return nil + } + return &moneyv1.Money{Amount: m.GetAmount(), Currency: m.GetCurrency()} +} + +func cloneMetadata(input map[string]string) map[string]string { + if len(input) == 0 { + return nil + } + clone := make(map[string]string, len(input)) + for k, v := range input { + clone[k] = v + } + return clone +} diff --git a/api/chain/gateway/internal/service/gateway/executor.go b/api/chain/gateway/internal/service/gateway/executor.go new file mode 100644 index 0000000..1972260 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/executor.go @@ -0,0 +1,385 @@ +package gateway + +import ( + "context" + "errors" + "math/big" + "strings" + "sync" + "time" + + "github.com/ethereum/go-ethereum" + "github.com/ethereum/go-ethereum/accounts/abi" + "github.com/ethereum/go-ethereum/common" + "github.com/ethereum/go-ethereum/core/types" + "github.com/ethereum/go-ethereum/ethclient" + "github.com/shopspring/decimal" + "go.uber.org/zap" + + "github.com/tech/sendico/chain/gateway/internal/keymanager" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" +) + +// TransferExecutor handles on-chain submission of transfers. +type TransferExecutor interface { + SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error) + AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error) +} + +// NewOnChainExecutor constructs a TransferExecutor that talks to an EVM-compatible chain. +func NewOnChainExecutor(logger mlogger.Logger, keyManager keymanager.Manager) TransferExecutor { + return &onChainExecutor{ + logger: logger.Named("executor"), + keyManager: keyManager, + clients: map[string]*ethclient.Client{}, + } +} + +type onChainExecutor struct { + logger mlogger.Logger + keyManager keymanager.Manager + + mu sync.Mutex + clients map[string]*ethclient.Client +} + +func (o *onChainExecutor) SubmitTransfer(ctx context.Context, transfer *model.Transfer, source *model.ManagedWallet, destinationAddress string, network Network) (string, error) { + if o.keyManager == nil { + o.logger.Error("key manager not configured") + return "", executorInternal("key manager is not configured", nil) + } + rpcURL := strings.TrimSpace(network.RPCURL) + if rpcURL == "" { + o.logger.Error("network rpc url missing", zap.String("network", network.Name)) + return "", executorInvalid("network rpc url is not configured") + } + if source == nil || transfer == nil { + o.logger.Error("transfer context missing") + return "", executorInvalid("transfer context missing") + } + if strings.TrimSpace(source.KeyReference) == "" { + o.logger.Error("source wallet missing key reference", zap.String("wallet_ref", source.WalletRef)) + return "", executorInvalid("source wallet missing key reference") + } + if strings.TrimSpace(source.DepositAddress) == "" { + o.logger.Error("source wallet missing deposit address", zap.String("wallet_ref", source.WalletRef)) + return "", executorInvalid("source wallet missing deposit address") + } + if !common.IsHexAddress(destinationAddress) { + o.logger.Error("invalid destination address", zap.String("transfer_ref", transfer.TransferRef), zap.String("address", destinationAddress)) + return "", executorInvalid("invalid destination address " + destinationAddress) + } + + o.logger.Info("submitting transfer", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("source_wallet_ref", source.WalletRef), + zap.String("network", network.Name), + zap.String("destination", strings.ToLower(destinationAddress)), + ) + + client, err := o.getClient(ctx, rpcURL) + if err != nil { + o.logger.Warn("failed to initialise rpc client", + zap.String("network", network.Name), + zap.String("rpc_url", rpcURL), + zap.Error(err), + ) + return "", err + } + + sourceAddress := common.HexToAddress(source.DepositAddress) + destination := common.HexToAddress(destinationAddress) + + ctx, cancel := context.WithTimeout(ctx, 60*time.Second) + defer cancel() + + nonce, err := client.PendingNonceAt(ctx, sourceAddress) + if err != nil { + o.logger.Warn("failed to fetch nonce", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("wallet_ref", source.WalletRef), + zap.Error(err), + ) + return "", executorInternal("failed to fetch nonce", err) + } + + gasPrice, err := client.SuggestGasPrice(ctx) + if err != nil { + o.logger.Warn("failed to suggest gas price", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("network", network.Name), + zap.Error(err), + ) + return "", executorInternal("failed to suggest gas price", err) + } + + var tx *types.Transaction + var txHash string + + chainID := new(big.Int).SetUint64(network.ChainID) + + if strings.TrimSpace(transfer.ContractAddress) == "" { + o.logger.Warn("native token transfer requested but not supported", zap.String("transfer_ref", transfer.TransferRef)) + return "", merrors.NotImplemented("executor: native token transfers not yet supported") + } + + if !common.IsHexAddress(transfer.ContractAddress) { + o.logger.Warn("invalid token contract address", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("contract", transfer.ContractAddress), + ) + return "", executorInvalid("invalid token contract address " + transfer.ContractAddress) + } + tokenAddress := common.HexToAddress(transfer.ContractAddress) + + decimals, err := erc20Decimals(ctx, client, tokenAddress) + if err != nil { + o.logger.Warn("failed to read token decimals", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("contract", transfer.ContractAddress), + zap.Error(err), + ) + return "", err + } + + amount := transfer.NetAmount + if amount == nil || strings.TrimSpace(amount.Amount) == "" { + o.logger.Warn("transfer missing net amount", zap.String("transfer_ref", transfer.TransferRef)) + return "", executorInvalid("transfer missing net amount") + } + amountInt, err := toBaseUnits(amount.Amount, decimals) + if err != nil { + o.logger.Warn("failed to convert amount to base units", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("amount", amount.Amount), + zap.Error(err), + ) + return "", err + } + + input, err := erc20ABI.Pack("transfer", destination, amountInt) + if err != nil { + o.logger.Warn("failed to encode transfer call", + zap.String("transfer_ref", transfer.TransferRef), + zap.Error(err), + ) + return "", executorInternal("failed to encode transfer call", err) + } + + callMsg := ethereum.CallMsg{ + From: sourceAddress, + To: &tokenAddress, + GasPrice: gasPrice, + Data: input, + } + gasLimit, err := client.EstimateGas(ctx, callMsg) + if err != nil { + o.logger.Warn("failed to estimate gas", + zap.String("transfer_ref", transfer.TransferRef), + zap.Error(err), + ) + return "", executorInternal("failed to estimate gas", err) + } + + tx = types.NewTransaction(nonce, tokenAddress, big.NewInt(0), gasLimit, gasPrice, input) + + signedTx, err := o.keyManager.SignTransaction(ctx, source.KeyReference, tx, chainID) + if err != nil { + o.logger.Warn("failed to sign transaction", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("wallet_ref", source.WalletRef), + zap.Error(err), + ) + return "", err + } + + if err := client.SendTransaction(ctx, signedTx); err != nil { + o.logger.Warn("failed to send transaction", + zap.String("transfer_ref", transfer.TransferRef), + zap.Error(err), + ) + return "", executorInternal("failed to send transaction", err) + } + + txHash = signedTx.Hash().Hex() + o.logger.Info("transaction submitted", + zap.String("transfer_ref", transfer.TransferRef), + zap.String("tx_hash", txHash), + zap.String("network", network.Name), + ) + + return txHash, nil +} + +func (o *onChainExecutor) getClient(ctx context.Context, rpcURL string) (*ethclient.Client, error) { + o.mu.Lock() + client, ok := o.clients[rpcURL] + o.mu.Unlock() + if ok { + return client, nil + } + + c, err := ethclient.DialContext(ctx, rpcURL) + if err != nil { + return nil, executorInternal("failed to connect to rpc "+rpcURL, err) + } + + o.mu.Lock() + defer o.mu.Unlock() + if existing, ok := o.clients[rpcURL]; ok { + // Another routine initialised it in the meantime; prefer the existing client and close the new one. + c.Close() + return existing, nil + } + o.clients[rpcURL] = c + return c, nil +} + +func (o *onChainExecutor) AwaitConfirmation(ctx context.Context, network Network, txHash string) (*types.Receipt, error) { + if strings.TrimSpace(txHash) == "" { + o.logger.Warn("missing transaction hash for confirmation", zap.String("network", network.Name)) + return nil, executorInvalid("tx hash is required") + } + rpcURL := strings.TrimSpace(network.RPCURL) + if rpcURL == "" { + o.logger.Warn("network rpc url missing while awaiting confirmation", zap.String("tx_hash", txHash)) + return nil, executorInvalid("network rpc url is not configured") + } + + client, err := o.getClient(ctx, rpcURL) + if err != nil { + return nil, err + } + + hash := common.HexToHash(txHash) + ticker := time.NewTicker(3 * time.Second) + defer ticker.Stop() + + for { + receipt, err := client.TransactionReceipt(ctx, hash) + if err != nil { + if errors.Is(err, ethereum.NotFound) { + select { + case <-ticker.C: + o.logger.Debug("transaction not yet mined", + zap.String("tx_hash", txHash), + zap.String("network", network.Name), + ) + continue + case <-ctx.Done(): + o.logger.Warn("context cancelled while awaiting confirmation", + zap.String("tx_hash", txHash), + zap.String("network", network.Name), + ) + return nil, ctx.Err() + } + } + o.logger.Warn("failed to fetch transaction receipt", + zap.String("tx_hash", txHash), + zap.String("network", network.Name), + zap.Error(err), + ) + return nil, executorInternal("failed to fetch transaction receipt", err) + } + o.logger.Info("transaction confirmed", + zap.String("tx_hash", txHash), + zap.String("network", network.Name), + zap.Uint64("block_number", receipt.BlockNumber.Uint64()), + zap.Uint64("status", receipt.Status), + ) + return receipt, nil + } +} + +var ( + erc20ABI abi.ABI +) + +func init() { + var err error + erc20ABI, err = abi.JSON(strings.NewReader(erc20ABIJSON)) + if err != nil { + panic("executor: failed to parse erc20 abi: " + err.Error()) + } +} + +const erc20ABIJSON = ` +[ + { + "constant": false, + "inputs": [ + { "name": "_to", "type": "address" }, + { "name": "_value", "type": "uint256" } + ], + "name": "transfer", + "outputs": [{ "name": "", "type": "bool" }], + "payable": false, + "stateMutability": "nonpayable", + "type": "function" + }, + { + "constant": true, + "inputs": [], + "name": "decimals", + "outputs": [{ "name": "", "type": "uint8" }], + "payable": false, + "stateMutability": "view", + "type": "function" + } +]` + +func erc20Decimals(ctx context.Context, client *ethclient.Client, token common.Address) (uint8, error) { + callData, err := erc20ABI.Pack("decimals") + if err != nil { + return 0, executorInternal("failed to encode decimals call", err) + } + msg := ethereum.CallMsg{ + To: &token, + Data: callData, + } + output, err := client.CallContract(ctx, msg, nil) + if err != nil { + return 0, executorInternal("decimals call failed", err) + } + values, err := erc20ABI.Unpack("decimals", output) + if err != nil { + return 0, executorInternal("failed to unpack decimals", err) + } + if len(values) == 0 { + return 0, executorInternal("decimals call returned no data", nil) + } + decimals, ok := values[0].(uint8) + if !ok { + return 0, executorInternal("decimals call returned unexpected type", nil) + } + return decimals, nil +} + +func toBaseUnits(amount string, decimals uint8) (*big.Int, error) { + value, err := decimal.NewFromString(strings.TrimSpace(amount)) + if err != nil { + return nil, executorInvalid("invalid amount " + amount + ": " + err.Error()) + } + if value.IsNegative() { + return nil, executorInvalid("amount must be positive") + } + multiplier := decimal.NewFromInt(1).Shift(int32(decimals)) + scaled := value.Mul(multiplier) + if !scaled.Equal(scaled.Truncate(0)) { + return nil, executorInvalid("amount " + amount + " exceeds token precision") + } + return scaled.BigInt(), nil +} + +func executorInvalid(msg string) error { + return merrors.InvalidArgument("executor: " + msg) +} + +func executorInternal(msg string, err error) error { + if err != nil { + msg = msg + ": " + err.Error() + } + return merrors.Internal("executor: " + msg) +} diff --git a/api/chain/gateway/internal/service/gateway/metrics.go b/api/chain/gateway/internal/service/gateway/metrics.go new file mode 100644 index 0000000..1f9aced --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/metrics.go @@ -0,0 +1,65 @@ +package gateway + +import ( + "errors" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/tech/sendico/pkg/merrors" +) + +var ( + metricsOnce sync.Once + + rpcLatency *prometheus.HistogramVec + rpcStatus *prometheus.CounterVec +) + +func initMetrics() { + metricsOnce.Do(func() { + rpcLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "sendico", + Subsystem: "chain_gateway", + Name: "rpc_latency_seconds", + Help: "Latency distribution for chain gateway RPC handlers.", + Buckets: prometheus.DefBuckets, + }, []string{"method"}) + + rpcStatus = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "sendico", + Subsystem: "chain_gateway", + Name: "rpc_requests_total", + Help: "Total number of RPC invocations grouped by method and status.", + }, []string{"method", "status"}) + }) +} + +func observeRPC(method string, err error, duration time.Duration) { + if rpcLatency != nil { + rpcLatency.WithLabelValues(method).Observe(duration.Seconds()) + } + if rpcStatus != nil { + rpcStatus.WithLabelValues(method, statusLabel(err)).Inc() + } +} + +func statusLabel(err error) string { + switch { + case err == nil: + return "ok" + case errors.Is(err, merrors.ErrInvalidArg): + return "invalid_argument" + case errors.Is(err, merrors.ErrNoData): + return "not_found" + case errors.Is(err, merrors.ErrDataConflict): + return "conflict" + case errors.Is(err, merrors.ErrAccessDenied): + return "denied" + case errors.Is(err, merrors.ErrInternal): + return "internal" + default: + return "error" + } +} diff --git a/api/chain/gateway/internal/service/gateway/options.go b/api/chain/gateway/internal/service/gateway/options.go new file mode 100644 index 0000000..aab4636 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/options.go @@ -0,0 +1,90 @@ +package gateway + +import ( + "strings" + + "github.com/tech/sendico/chain/gateway/internal/keymanager" + clockpkg "github.com/tech/sendico/pkg/clock" +) + +// Option configures the Service. +type Option func(*Service) + +// Network describes a supported blockchain network and known token contracts. +type Network struct { + Name string + RPCURL string + ChainID uint64 + NativeToken string + TokenConfigs []TokenContract +} + +// TokenContract captures the metadata needed to work with a specific on-chain token. +type TokenContract struct { + Symbol string + ContractAddress string +} + +// ServiceWallet captures the managed service wallet configuration. +type ServiceWallet struct { + Network string + Address string + PrivateKey string +} + +// WithKeyManager configures the service key manager. +func WithKeyManager(manager keymanager.Manager) Option { + return func(s *Service) { + s.keyManager = manager + } +} + +// WithTransferExecutor configures the executor responsible for on-chain submissions. +func WithTransferExecutor(executor TransferExecutor) Option { + return func(s *Service) { + s.executor = executor + } +} + +// WithNetworks configures supported blockchain networks. +func WithNetworks(networks []Network) Option { + return func(s *Service) { + if len(networks) == 0 { + return + } + if s.networks == nil { + s.networks = make(map[string]Network, len(networks)) + } + for _, network := range networks { + if network.Name == "" { + continue + } + clone := network + if clone.TokenConfigs == nil { + clone.TokenConfigs = []TokenContract{} + } + for i := range clone.TokenConfigs { + clone.TokenConfigs[i].Symbol = strings.ToUpper(strings.TrimSpace(clone.TokenConfigs[i].Symbol)) + clone.TokenConfigs[i].ContractAddress = strings.ToLower(strings.TrimSpace(clone.TokenConfigs[i].ContractAddress)) + } + clone.Name = strings.ToLower(strings.TrimSpace(clone.Name)) + s.networks[clone.Name] = clone + } + } +} + +// WithServiceWallet configures the service wallet binding. +func WithServiceWallet(wallet ServiceWallet) Option { + return func(s *Service) { + s.serviceWallet = wallet + } +} + +// WithClock overrides the service clock. +func WithClock(clk clockpkg.Clock) Option { + return func(s *Service) { + if clk != nil { + s.clock = clk + } + } +} diff --git a/api/chain/gateway/internal/service/gateway/service.go b/api/chain/gateway/internal/service/gateway/service.go new file mode 100644 index 0000000..4affea2 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/service.go @@ -0,0 +1,214 @@ +package gateway + +import ( + "context" + "strings" + + gatewayv1 "github.com/tech/sendico/chain/gateway/internal/generated/service/gateway/v1" + "github.com/tech/sendico/chain/gateway/internal/keymanager" + "github.com/tech/sendico/chain/gateway/storage" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + clockpkg "github.com/tech/sendico/pkg/clock" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/grpc" +) + +type serviceError string + +func (e serviceError) Error() string { + return string(e) +} + +var ( + errStorageUnavailable = serviceError("chain_gateway: storage not initialised") +) + +// Service implements the ChainGatewayService RPC contract. +type Service struct { + logger mlogger.Logger + storage storage.Repository + producer msg.Producer + clock clockpkg.Clock + + networks map[string]Network + serviceWallet ServiceWallet + keyManager keymanager.Manager + executor TransferExecutor + + gatewayv1.UnimplementedChainGatewayServiceServer +} + +// NewService constructs the chain gateway service skeleton. +func NewService(logger mlogger.Logger, repo storage.Repository, producer msg.Producer, opts ...Option) *Service { + svc := &Service{ + logger: logger.Named("chain_gateway"), + storage: repo, + producer: producer, + clock: clockpkg.System{}, + networks: map[string]Network{}, + } + + initMetrics() + + for _, opt := range opts { + if opt != nil { + opt(svc) + } + } + + if svc.clock == nil { + svc.clock = clockpkg.System{} + } + if svc.networks == nil { + svc.networks = map[string]Network{} + } + + return svc +} + +// Register wires the service onto the provided gRPC router. +func (s *Service) Register(router routers.GRPC) error { + return router.Register(func(reg grpc.ServiceRegistrar) { + gatewayv1.RegisterChainGatewayServiceServer(reg, s) + }) +} + +func (s *Service) CreateManagedWallet(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) (*gatewayv1.CreateManagedWalletResponse, error) { + return executeUnary(ctx, s, "CreateManagedWallet", s.createManagedWalletHandler, req) +} + +func (s *Service) GetManagedWallet(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) (*gatewayv1.GetManagedWalletResponse, error) { + return executeUnary(ctx, s, "GetManagedWallet", s.getManagedWalletHandler, req) +} + +func (s *Service) ListManagedWallets(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) (*gatewayv1.ListManagedWalletsResponse, error) { + return executeUnary(ctx, s, "ListManagedWallets", s.listManagedWalletsHandler, req) +} + +func (s *Service) GetWalletBalance(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) (*gatewayv1.GetWalletBalanceResponse, error) { + return executeUnary(ctx, s, "GetWalletBalance", s.getWalletBalanceHandler, req) +} + +func (s *Service) SubmitTransfer(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) { + return executeUnary(ctx, s, "SubmitTransfer", s.submitTransferHandler, req) +} + +func (s *Service) GetTransfer(ctx context.Context, req *gatewayv1.GetTransferRequest) (*gatewayv1.GetTransferResponse, error) { + return executeUnary(ctx, s, "GetTransfer", s.getTransferHandler, req) +} + +func (s *Service) ListTransfers(ctx context.Context, req *gatewayv1.ListTransfersRequest) (*gatewayv1.ListTransfersResponse, error) { + return executeUnary(ctx, s, "ListTransfers", s.listTransfersHandler, req) +} + +func (s *Service) EstimateTransferFee(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) (*gatewayv1.EstimateTransferFeeResponse, error) { + return executeUnary(ctx, s, "EstimateTransferFee", s.estimateTransferFeeHandler, req) +} + +func (s *Service) ensureRepository(ctx context.Context) error { + if s.storage == nil { + return errStorageUnavailable + } + return s.storage.Ping(ctx) +} + +func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) { + start := svc.clock.Now() + resp, err := gsresponse.Unary(svc.logger, mservice.ChainGateway, handler)(ctx, req) + observeRPC(method, err, svc.clock.Now().Sub(start)) + return resp, err +} + +func resolveContractAddress(tokens []TokenContract, symbol string) string { + upper := strings.ToUpper(symbol) + for _, token := range tokens { + if strings.EqualFold(token.Symbol, upper) && token.ContractAddress != "" { + return strings.ToLower(token.ContractAddress) + } + } + return "" +} + +func generateWalletRef() string { + return primitive.NewObjectID().Hex() +} + +func generateTransferRef() string { + return primitive.NewObjectID().Hex() +} + +func chainKeyFromEnum(chain gatewayv1.ChainNetwork) (string, gatewayv1.ChainNetwork) { + if name, ok := gatewayv1.ChainNetwork_name[int32(chain)]; ok { + key := strings.ToLower(strings.TrimPrefix(name, "CHAIN_NETWORK_")) + return key, chain + } + return "", gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED +} + +func chainEnumFromName(name string) gatewayv1.ChainNetwork { + if name == "" { + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED + } + upper := strings.ToUpper(strings.ReplaceAll(strings.ReplaceAll(name, " ", "_"), "-", "_")) + key := "CHAIN_NETWORK_" + upper + if val, ok := gatewayv1.ChainNetwork_value[key]; ok { + return gatewayv1.ChainNetwork(val) + } + return gatewayv1.ChainNetwork_CHAIN_NETWORK_UNSPECIFIED +} + +func managedWalletStatusToProto(status model.ManagedWalletStatus) gatewayv1.ManagedWalletStatus { + switch status { + case model.ManagedWalletStatusActive: + return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_ACTIVE + case model.ManagedWalletStatusSuspended: + return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_SUSPENDED + case model.ManagedWalletStatusClosed: + return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_CLOSED + default: + return gatewayv1.ManagedWalletStatus_MANAGED_WALLET_STATUS_UNSPECIFIED + } +} + +func transferStatusToModel(status gatewayv1.TransferStatus) model.TransferStatus { + switch status { + case gatewayv1.TransferStatus_TRANSFER_PENDING: + return model.TransferStatusPending + case gatewayv1.TransferStatus_TRANSFER_SIGNING: + return model.TransferStatusSigning + case gatewayv1.TransferStatus_TRANSFER_SUBMITTED: + return model.TransferStatusSubmitted + case gatewayv1.TransferStatus_TRANSFER_CONFIRMED: + return model.TransferStatusConfirmed + case gatewayv1.TransferStatus_TRANSFER_FAILED: + return model.TransferStatusFailed + case gatewayv1.TransferStatus_TRANSFER_CANCELLED: + return model.TransferStatusCancelled + default: + return "" + } +} + +func transferStatusToProto(status model.TransferStatus) gatewayv1.TransferStatus { + switch status { + case model.TransferStatusPending: + return gatewayv1.TransferStatus_TRANSFER_PENDING + case model.TransferStatusSigning: + return gatewayv1.TransferStatus_TRANSFER_SIGNING + case model.TransferStatusSubmitted: + return gatewayv1.TransferStatus_TRANSFER_SUBMITTED + case model.TransferStatusConfirmed: + return gatewayv1.TransferStatus_TRANSFER_CONFIRMED + case model.TransferStatusFailed: + return gatewayv1.TransferStatus_TRANSFER_FAILED + case model.TransferStatusCancelled: + return gatewayv1.TransferStatus_TRANSFER_CANCELLED + default: + return gatewayv1.TransferStatus_TRANSFER_STATUS_UNSPECIFIED + } +} diff --git a/api/chain/gateway/internal/service/gateway/service_test.go b/api/chain/gateway/internal/service/gateway/service_test.go new file mode 100644 index 0000000..61a41e6 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/service_test.go @@ -0,0 +1,556 @@ +package gateway + +import ( + "context" + "fmt" + "math/big" + "sort" + "strings" + "sync" + "testing" + "time" + + "github.com/stretchr/testify/require" + igatewayv1 "github.com/tech/sendico/chain/gateway/internal/generated/service/gateway/v1" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + + "github.com/tech/sendico/chain/gateway/internal/keymanager" + "github.com/tech/sendico/chain/gateway/storage" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/merrors" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" + + "github.com/ethereum/go-ethereum/core/types" +) + +const ( + walletDefaultLimit int64 = 50 + walletMaxLimit int64 = 200 + transferDefaultLimit int64 = 50 + transferMaxLimit int64 = 200 + depositDefaultLimit int64 = 100 + depositMaxLimit int64 = 500 +) + +func TestCreateManagedWallet_Idempotent(t *testing.T) { + svc, repo := newTestService(t) + + ctx := context.Background() + req := &igatewayv1.CreateManagedWalletRequest{ + IdempotencyKey: "idem-1", + OrganizationRef: "org-1", + OwnerRef: "owner-1", + Asset: &igatewayv1.Asset{ + Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, + TokenSymbol: "USDC", + }, + } + + resp, err := svc.CreateManagedWallet(ctx, req) + require.NoError(t, err) + require.NotNil(t, resp.GetWallet()) + firstRef := resp.GetWallet().GetWalletRef() + require.NotEmpty(t, firstRef) + + resp2, err := svc.CreateManagedWallet(ctx, req) + require.NoError(t, err) + require.Equal(t, firstRef, resp2.GetWallet().GetWalletRef()) + + // ensure stored only once + require.Equal(t, 1, repo.wallets.count()) +} + +func TestSubmitTransfer_ManagedDestination(t *testing.T) { + svc, repo := newTestService(t) + ctx := context.Background() + + // create source wallet + srcResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{ + IdempotencyKey: "idem-src", + OrganizationRef: "org-1", + OwnerRef: "owner-1", + Asset: &igatewayv1.Asset{ + Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, + TokenSymbol: "USDC", + }, + }) + require.NoError(t, err) + srcRef := srcResp.GetWallet().GetWalletRef() + + // destination wallet + dstResp, err := svc.CreateManagedWallet(ctx, &igatewayv1.CreateManagedWalletRequest{ + IdempotencyKey: "idem-dst", + OrganizationRef: "org-1", + OwnerRef: "owner-2", + Asset: &igatewayv1.Asset{ + Chain: igatewayv1.ChainNetwork_CHAIN_NETWORK_ETHEREUM_MAINNET, + TokenSymbol: "USDC", + }, + }) + require.NoError(t, err) + dstRef := dstResp.GetWallet().GetWalletRef() + + transferResp, err := svc.SubmitTransfer(ctx, &igatewayv1.SubmitTransferRequest{ + IdempotencyKey: "transfer-1", + OrganizationRef: "org-1", + SourceWalletRef: srcRef, + Destination: &igatewayv1.TransferDestination{ + Destination: &igatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: dstRef}, + }, + Amount: &moneyv1.Money{Currency: "USDC", Amount: "100"}, + Fees: []*igatewayv1.ServiceFeeBreakdown{ + { + FeeCode: "service", + Amount: &moneyv1.Money{Currency: "USDC", Amount: "5"}, + }, + }, + }) + require.NoError(t, err) + require.NotNil(t, transferResp.GetTransfer()) + require.Equal(t, "95", transferResp.GetTransfer().GetNetAmount().GetAmount()) + + stored := repo.transfers.get(transferResp.GetTransfer().GetTransferRef()) + require.NotNil(t, stored) + require.Equal(t, model.TransferStatusPending, stored.Status) + + // GetTransfer + getResp, err := svc.GetTransfer(ctx, &igatewayv1.GetTransferRequest{TransferRef: stored.TransferRef}) + require.NoError(t, err) + require.Equal(t, stored.TransferRef, getResp.GetTransfer().GetTransferRef()) + + // ListTransfers + listResp, err := svc.ListTransfers(ctx, &igatewayv1.ListTransfersRequest{ + SourceWalletRef: srcRef, + Page: &paginationv1.CursorPageRequest{Limit: 10}, + }) + require.NoError(t, err) + require.Len(t, listResp.GetTransfers(), 1) + require.Equal(t, stored.TransferRef, listResp.GetTransfers()[0].GetTransferRef()) +} + +func TestGetWalletBalance_NotFound(t *testing.T) { + svc, _ := newTestService(t) + ctx := context.Background() + + _, err := svc.GetWalletBalance(ctx, &igatewayv1.GetWalletBalanceRequest{WalletRef: "missing"}) + require.Error(t, err) + st, _ := status.FromError(err) + require.Equal(t, codes.NotFound, st.Code()) +} + +// ---- in-memory storage implementation ---- + +type inMemoryRepository struct { + wallets *inMemoryWallets + transfers *inMemoryTransfers + deposits *inMemoryDeposits +} + +func newInMemoryRepository() *inMemoryRepository { + return &inMemoryRepository{ + wallets: newInMemoryWallets(), + transfers: newInMemoryTransfers(), + deposits: newInMemoryDeposits(), + } +} + +func (r *inMemoryRepository) Ping(context.Context) error { return nil } +func (r *inMemoryRepository) Wallets() storage.WalletsStore { return r.wallets } +func (r *inMemoryRepository) Transfers() storage.TransfersStore { return r.transfers } +func (r *inMemoryRepository) Deposits() storage.DepositsStore { return r.deposits } + +// Wallets store + +type inMemoryWallets struct { + mu sync.Mutex + wallets map[string]*model.ManagedWallet + byIdemp map[string]string + balances map[string]*model.WalletBalance +} + +func newInMemoryWallets() *inMemoryWallets { + return &inMemoryWallets{ + wallets: make(map[string]*model.ManagedWallet), + byIdemp: make(map[string]string), + balances: make(map[string]*model.WalletBalance), + } +} + +func (w *inMemoryWallets) count() int { + w.mu.Lock() + defer w.mu.Unlock() + return len(w.wallets) +} + +func (w *inMemoryWallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*model.ManagedWallet, error) { + w.mu.Lock() + defer w.mu.Unlock() + + if wallet == nil { + return nil, merrors.InvalidArgument("walletsStore: nil wallet") + } + wallet.Normalize() + if wallet.IdempotencyKey == "" { + return nil, merrors.InvalidArgument("walletsStore: empty idempotencyKey") + } + + if existingRef, ok := w.byIdemp[wallet.IdempotencyKey]; ok { + existing := w.wallets[existingRef] + return existing, merrors.ErrDataConflict + } + + if wallet.WalletRef == "" { + wallet.WalletRef = primitive.NewObjectID().Hex() + } + if wallet.GetID() == nil || wallet.GetID().IsZero() { + wallet.SetID(primitive.NewObjectID()) + } else { + wallet.Update() + } + + w.wallets[wallet.WalletRef] = wallet + w.byIdemp[wallet.IdempotencyKey] = wallet.WalletRef + return wallet, nil +} + +func (w *inMemoryWallets) Get(ctx context.Context, walletRef string) (*model.ManagedWallet, error) { + w.mu.Lock() + defer w.mu.Unlock() + wallet, ok := w.wallets[strings.TrimSpace(walletRef)] + if !ok { + return nil, merrors.NoData("wallet not found") + } + return wallet, nil +} + +func (w *inMemoryWallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*model.ManagedWalletList, error) { + w.mu.Lock() + defer w.mu.Unlock() + + items := make([]*model.ManagedWallet, 0, len(w.wallets)) + for _, wallet := range w.wallets { + if filter.OrganizationRef != "" && !strings.EqualFold(wallet.OrganizationRef, filter.OrganizationRef) { + continue + } + if filter.OwnerRef != "" && !strings.EqualFold(wallet.OwnerRef, filter.OwnerRef) { + continue + } + if filter.Network != "" && !strings.EqualFold(wallet.Network, filter.Network) { + continue + } + if filter.TokenSymbol != "" && !strings.EqualFold(wallet.TokenSymbol, filter.TokenSymbol) { + continue + } + items = append(items, wallet) + } + + sort.Slice(items, func(i, j int) bool { + return items[i].ID.Timestamp().Before(items[j].ID.Timestamp()) + }) + + startIndex := 0 + if cursor := strings.TrimSpace(filter.Cursor); cursor != "" { + if oid, err := primitive.ObjectIDFromHex(cursor); err == nil { + for idx, item := range items { + if item.ID.Timestamp().After(oid.Timestamp()) { + startIndex = idx + break + } + } + } + } + + limit := int(sanitizeLimit(filter.Limit, walletDefaultLimit, walletMaxLimit)) + end := startIndex + limit + hasMore := false + if end < len(items) { + hasMore = true + items = items[startIndex:end] + } else { + items = items[startIndex:] + } + + nextCursor := "" + if hasMore && len(items) > 0 { + nextCursor = items[len(items)-1].ID.Hex() + } + + return &model.ManagedWalletList{Items: items, NextCursor: nextCursor}, nil +} + +func (w *inMemoryWallets) SaveBalance(ctx context.Context, balance *model.WalletBalance) error { + w.mu.Lock() + defer w.mu.Unlock() + if balance == nil { + return merrors.InvalidArgument("walletsStore: nil balance") + } + balance.Normalize() + if balance.WalletRef == "" { + return merrors.InvalidArgument("walletsStore: empty walletRef for balance") + } + if balance.CalculatedAt.IsZero() { + balance.CalculatedAt = time.Now().UTC() + } + existing, ok := w.balances[balance.WalletRef] + if !ok { + if balance.GetID() == nil || balance.GetID().IsZero() { + balance.SetID(primitive.NewObjectID()) + } + w.balances[balance.WalletRef] = balance + return nil + } + existing.Available = balance.Available + existing.PendingInbound = balance.PendingInbound + existing.PendingOutbound = balance.PendingOutbound + existing.CalculatedAt = balance.CalculatedAt + existing.Update() + return nil +} + +func (w *inMemoryWallets) GetBalance(ctx context.Context, walletRef string) (*model.WalletBalance, error) { + w.mu.Lock() + defer w.mu.Unlock() + balance, ok := w.balances[strings.TrimSpace(walletRef)] + if !ok { + return nil, merrors.NoData("wallet balance not found") + } + return balance, nil +} + +// Transfers store + +type inMemoryTransfers struct { + mu sync.Mutex + items map[string]*model.Transfer + byIdemp map[string]string +} + +func newInMemoryTransfers() *inMemoryTransfers { + return &inMemoryTransfers{ + items: make(map[string]*model.Transfer), + byIdemp: make(map[string]string), + } +} + +func (t *inMemoryTransfers) Create(ctx context.Context, transfer *model.Transfer) (*model.Transfer, error) { + t.mu.Lock() + defer t.mu.Unlock() + if transfer == nil { + return nil, merrors.InvalidArgument("transfersStore: nil transfer") + } + transfer.Normalize() + if transfer.IdempotencyKey == "" { + return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey") + } + if ref, ok := t.byIdemp[transfer.IdempotencyKey]; ok { + return t.items[ref], merrors.ErrDataConflict + } + if transfer.TransferRef == "" { + transfer.TransferRef = primitive.NewObjectID().Hex() + } + if transfer.GetID() == nil || transfer.GetID().IsZero() { + transfer.SetID(primitive.NewObjectID()) + } else { + transfer.Update() + } + t.items[transfer.TransferRef] = transfer + t.byIdemp[transfer.IdempotencyKey] = transfer.TransferRef + return transfer, nil +} + +func (t *inMemoryTransfers) Get(ctx context.Context, transferRef string) (*model.Transfer, error) { + t.mu.Lock() + defer t.mu.Unlock() + transfer, ok := t.items[strings.TrimSpace(transferRef)] + if !ok { + return nil, merrors.NoData("transfer not found") + } + return transfer, nil +} + +func (t *inMemoryTransfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) { + t.mu.Lock() + defer t.mu.Unlock() + items := make([]*model.Transfer, 0, len(t.items)) + for _, transfer := range t.items { + if filter.SourceWalletRef != "" && !strings.EqualFold(transfer.SourceWalletRef, filter.SourceWalletRef) { + continue + } + if filter.DestinationWalletRef != "" && !strings.EqualFold(transfer.Destination.ManagedWalletRef, filter.DestinationWalletRef) { + continue + } + if filter.Status != "" && transfer.Status != filter.Status { + continue + } + items = append(items, transfer) + } + + sort.Slice(items, func(i, j int) bool { + return items[i].ID.Timestamp().Before(items[j].ID.Timestamp()) + }) + + start := 0 + if cursor := strings.TrimSpace(filter.Cursor); cursor != "" { + if oid, err := primitive.ObjectIDFromHex(cursor); err == nil { + for idx, item := range items { + if item.ID.Timestamp().After(oid.Timestamp()) { + start = idx + break + } + } + } + } + + limit := int(sanitizeLimit(filter.Limit, transferDefaultLimit, transferMaxLimit)) + end := start + limit + hasMore := false + if end < len(items) { + hasMore = true + items = items[start:end] + } else { + items = items[start:] + } + + nextCursor := "" + if hasMore && len(items) > 0 { + nextCursor = items[len(items)-1].ID.Hex() + } + + return &model.TransferList{Items: items, NextCursor: nextCursor}, nil +} + +func (t *inMemoryTransfers) UpdateStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason string, txHash string) (*model.Transfer, error) { + t.mu.Lock() + defer t.mu.Unlock() + transfer, ok := t.items[strings.TrimSpace(transferRef)] + if !ok { + return nil, merrors.NoData("transfer not found") + } + transfer.Status = status + if status == model.TransferStatusFailed { + transfer.FailureReason = strings.TrimSpace(failureReason) + } else { + transfer.FailureReason = "" + } + transfer.TxHash = strings.TrimSpace(txHash) + transfer.LastStatusAt = time.Now().UTC() + transfer.Update() + return transfer, nil +} + +// helper for tests +func (t *inMemoryTransfers) get(ref string) *model.Transfer { + t.mu.Lock() + defer t.mu.Unlock() + return t.items[ref] +} + +// Deposits store (minimal for tests) + +type inMemoryDeposits struct { + mu sync.Mutex + items map[string]*model.Deposit +} + +func newInMemoryDeposits() *inMemoryDeposits { + return &inMemoryDeposits{items: make(map[string]*model.Deposit)} +} + +func (d *inMemoryDeposits) Record(ctx context.Context, deposit *model.Deposit) error { + d.mu.Lock() + defer d.mu.Unlock() + if deposit == nil { + return merrors.InvalidArgument("depositsStore: nil deposit") + } + deposit.Normalize() + if deposit.DepositRef == "" { + return merrors.InvalidArgument("depositsStore: empty depositRef") + } + if existing, ok := d.items[deposit.DepositRef]; ok { + existing.Status = deposit.Status + existing.LastStatusAt = time.Now().UTC() + existing.Update() + return nil + } + if deposit.GetID() == nil || deposit.GetID().IsZero() { + deposit.SetID(primitive.NewObjectID()) + } + if deposit.ObservedAt.IsZero() { + deposit.ObservedAt = time.Now().UTC() + } + if deposit.RecordedAt.IsZero() { + deposit.RecordedAt = time.Now().UTC() + } + deposit.LastStatusAt = time.Now().UTC() + d.items[deposit.DepositRef] = deposit + return nil +} + +func (d *inMemoryDeposits) ListPending(ctx context.Context, network string, limit int32) ([]*model.Deposit, error) { + d.mu.Lock() + defer d.mu.Unlock() + results := make([]*model.Deposit, 0) + for _, deposit := range d.items { + if deposit.Status != model.DepositStatusPending { + continue + } + if network != "" && !strings.EqualFold(deposit.Network, network) { + continue + } + results = append(results, deposit) + } + sort.Slice(results, func(i, j int) bool { + return results[i].ObservedAt.Before(results[j].ObservedAt) + }) + limitVal := int(sanitizeLimit(limit, depositDefaultLimit, depositMaxLimit)) + if len(results) > limitVal { + results = results[:limitVal] + } + return results, nil +} + +// shared helpers + +func sanitizeLimit(requested int32, def, max int64) int64 { + if requested <= 0 { + return def + } + if requested > int32(max) { + return max + } + return int64(requested) +} + +func newTestService(_ *testing.T) (*Service, *inMemoryRepository) { + repo := newInMemoryRepository() + logger := zap.NewNop() + svc := NewService(logger, repo, nil, + WithKeyManager(&fakeKeyManager{}), + WithNetworks([]Network{{ + Name: "ethereum_mainnet", + TokenConfigs: []TokenContract{ + {Symbol: "USDC", ContractAddress: "0xusdc"}, + }, + }}), + WithServiceWallet(ServiceWallet{Network: "ethereum_mainnet", Address: "0xservice"}), + ) + return svc, repo +} + +type fakeKeyManager struct{} + +func (f *fakeKeyManager) CreateManagedWalletKey(ctx context.Context, walletRef string, network string) (*keymanager.ManagedWalletKey, error) { + return &keymanager.ManagedWalletKey{ + KeyID: fmt.Sprintf("%s/%s", strings.ToLower(network), walletRef), + Address: "0x" + strings.Repeat("a", 40), + PublicKey: strings.Repeat("b", 128), + }, nil +} + +func (f *fakeKeyManager) SignTransaction(ctx context.Context, keyID string, tx *types.Transaction, chainID *big.Int) (*types.Transaction, error) { + return tx, nil +} diff --git a/api/chain/gateway/internal/service/gateway/transfer_execution.go b/api/chain/gateway/internal/service/gateway/transfer_execution.go new file mode 100644 index 0000000..252b574 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/transfer_execution.go @@ -0,0 +1,99 @@ +package gateway + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/ethereum/go-ethereum/core/types" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/merrors" + "go.uber.org/zap" +) + +func (s *Service) launchTransferExecution(transferRef, sourceWalletRef string, network Network) { + if s.executor == nil { + return + } + + go func(ref, walletRef string, net Network) { + ctx, cancel := context.WithTimeout(context.Background(), 15*time.Minute) + defer cancel() + + if err := s.executeTransfer(ctx, ref, walletRef, net); err != nil { + s.logger.Error("failed to execute transfer", zap.String("transfer_ref", ref), zap.Error(err)) + } + }(transferRef, sourceWalletRef, network) +} + +func (s *Service) executeTransfer(ctx context.Context, transferRef, sourceWalletRef string, network Network) error { + transfer, err := s.storage.Transfers().Get(ctx, transferRef) + if err != nil { + return err + } + + sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef) + if err != nil { + return err + } + + if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSigning, "", ""); err != nil { + s.logger.Warn("failed to update transfer status to signing", zap.String("transfer_ref", transferRef), zap.Error(err)) + } + + destinationAddress, err := s.destinationAddress(ctx, transfer.Destination) + if err != nil { + _, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + return err + } + + txHash, err := s.executor.SubmitTransfer(ctx, transfer, sourceWallet, destinationAddress, network) + if err != nil { + _, _ = s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, err.Error(), "") + return err + } + + if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusSubmitted, "", txHash); err != nil { + s.logger.Warn("failed to update transfer status to submitted", zap.String("transfer_ref", transferRef), zap.Error(err)) + } + + receiptCtx, cancel := context.WithTimeout(ctx, 10*time.Minute) + defer cancel() + receipt, err := s.executor.AwaitConfirmation(receiptCtx, network, txHash) + if err != nil { + if !errors.Is(err, context.DeadlineExceeded) && !errors.Is(err, context.Canceled) { + s.logger.Warn("failed to await transfer confirmation", zap.String("transfer_ref", transferRef), zap.Error(err)) + } + return err + } + + if receipt != nil && receipt.Status == types.ReceiptStatusSuccessful { + if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusConfirmed, "", txHash); err != nil { + s.logger.Warn("failed to update transfer status to confirmed", zap.String("transfer_ref", transferRef), zap.Error(err)) + } + return nil + } + + if _, err := s.storage.Transfers().UpdateStatus(ctx, transferRef, model.TransferStatusFailed, "transaction reverted", txHash); err != nil { + s.logger.Warn("failed to update transfer status to failed", zap.String("transfer_ref", transferRef), zap.Error(err)) + } + return nil +} + +func (s *Service) destinationAddress(ctx context.Context, dest model.TransferDestination) (string, error) { + if ref := strings.TrimSpace(dest.ManagedWalletRef); ref != "" { + wallet, err := s.storage.Wallets().Get(ctx, ref) + if err != nil { + return "", err + } + if strings.TrimSpace(wallet.DepositAddress) == "" { + return "", merrors.Internal("destination wallet missing deposit address") + } + return wallet.DepositAddress, nil + } + if addr := strings.TrimSpace(dest.ExternalAddress); addr != "" { + return strings.ToLower(addr), nil + } + return "", merrors.InvalidArgument("transfer destination address not resolved") +} diff --git a/api/chain/gateway/internal/service/gateway/transfer_handlers.go b/api/chain/gateway/internal/service/gateway/transfer_handlers.go new file mode 100644 index 0000000..43593c8 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/transfer_handlers.go @@ -0,0 +1,309 @@ +package gateway + +import ( + "context" + "errors" + "strings" + + gatewayv1 "github.com/tech/sendico/chain/gateway/internal/generated/service/gateway/v1" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mservice" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" + "github.com/shopspring/decimal" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *Service) submitTransferHandler(ctx context.Context, req *gatewayv1.SubmitTransferRequest) gsresponse.Responder[gatewayv1.SubmitTransferResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + if req == nil { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request")) + } + + idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey()) + if idempotencyKey == "" { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required")) + } + organizationRef := strings.TrimSpace(req.GetOrganizationRef()) + if organizationRef == "" { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required")) + } + sourceWalletRef := strings.TrimSpace(req.GetSourceWalletRef()) + if sourceWalletRef == "" { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("source_wallet_ref is required")) + } + amount := req.GetAmount() + if amount == nil { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required")) + } + amountCurrency := strings.ToUpper(strings.TrimSpace(amount.GetCurrency())) + if amountCurrency == "" { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.currency is required")) + } + amountValue := strings.TrimSpace(amount.GetAmount()) + if amountValue == "" { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount.amount is required")) + } + + sourceWallet, err := s.storage.Wallets().Get(ctx, sourceWalletRef) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + if !strings.EqualFold(sourceWallet.OrganizationRef, organizationRef) { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref mismatch with wallet")) + } + networkKey := strings.ToLower(strings.TrimSpace(sourceWallet.Network)) + networkCfg, ok := s.networks[networkKey] + if !ok { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain for wallet")) + } + + destination, err := s.resolveDestination(ctx, req.GetDestination(), sourceWallet) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + return gsresponse.NotFound[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + + fees, feeSum, err := convertFees(req.GetFees(), amountCurrency) + if err != nil { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + amountDec, err := decimal.NewFromString(amountValue) + if err != nil { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("invalid amount")) + } + netDec := amountDec.Sub(feeSum) + if netDec.IsNegative() { + return gsresponse.InvalidArgument[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("fees exceed amount")) + } + + netAmount := cloneMoney(amount) + netAmount.Amount = netDec.String() + + transfer := &model.Transfer{ + IdempotencyKey: idempotencyKey, + TransferRef: generateTransferRef(), + OrganizationRef: organizationRef, + SourceWalletRef: sourceWalletRef, + Destination: destination, + Network: sourceWallet.Network, + TokenSymbol: sourceWallet.TokenSymbol, + ContractAddress: sourceWallet.ContractAddress, + RequestedAmount: cloneMoney(amount), + NetAmount: netAmount, + Fees: fees, + Status: model.TransferStatusPending, + ClientReference: strings.TrimSpace(req.GetClientReference()), + LastStatusAt: s.clock.Now().UTC(), + } + + saved, err := s.storage.Transfers().Create(ctx, transfer) + if err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + s.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", idempotencyKey)) + return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)}) + } + return gsresponse.Auto[gatewayv1.SubmitTransferResponse](s.logger, mservice.ChainGateway, err) + } + + if s.executor != nil { + s.launchTransferExecution(saved.TransferRef, sourceWalletRef, networkCfg) + } + + return gsresponse.Success(&gatewayv1.SubmitTransferResponse{Transfer: s.toProtoTransfer(saved)}) +} + +func (s *Service) getTransferHandler(ctx context.Context, req *gatewayv1.GetTransferRequest) gsresponse.Responder[gatewayv1.GetTransferResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err) + } + if req == nil { + return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request")) + } + transferRef := strings.TrimSpace(req.GetTransferRef()) + if transferRef == "" { + return gsresponse.InvalidArgument[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("transfer_ref is required")) + } + transfer, err := s.storage.Transfers().Get(ctx, transferRef) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + return gsresponse.NotFound[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Auto[gatewayv1.GetTransferResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Success(&gatewayv1.GetTransferResponse{Transfer: s.toProtoTransfer(transfer)}) +} + +func (s *Service) listTransfersHandler(ctx context.Context, req *gatewayv1.ListTransfersRequest) gsresponse.Responder[gatewayv1.ListTransfersResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err) + } + filter := model.TransferFilter{} + if req != nil { + filter.SourceWalletRef = strings.TrimSpace(req.GetSourceWalletRef()) + filter.DestinationWalletRef = strings.TrimSpace(req.GetDestinationWalletRef()) + if status := transferStatusToModel(req.GetStatus()); status != "" { + filter.Status = status + } + if page := req.GetPage(); page != nil { + filter.Cursor = strings.TrimSpace(page.GetCursor()) + filter.Limit = page.GetLimit() + } + } + + result, err := s.storage.Transfers().List(ctx, filter) + if err != nil { + return gsresponse.Auto[gatewayv1.ListTransfersResponse](s.logger, mservice.ChainGateway, err) + } + + protoTransfers := make([]*gatewayv1.Transfer, 0, len(result.Items)) + for _, transfer := range result.Items { + protoTransfers = append(protoTransfers, s.toProtoTransfer(transfer)) + } + + resp := &gatewayv1.ListTransfersResponse{ + Transfers: protoTransfers, + Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor}, + } + return gsresponse.Success(resp) +} + +func (s *Service) estimateTransferFeeHandler(ctx context.Context, req *gatewayv1.EstimateTransferFeeRequest) gsresponse.Responder[gatewayv1.EstimateTransferFeeResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, err) + } + if req == nil || req.GetAmount() == nil { + return gsresponse.InvalidArgument[gatewayv1.EstimateTransferFeeResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("amount is required")) + } + currency := req.GetAmount().GetCurrency() + fee := &moneyv1.Money{ + Currency: currency, + Amount: "0", + } + resp := &gatewayv1.EstimateTransferFeeResponse{ + NetworkFee: fee, + EstimationContext: "not_implemented", + } + return gsresponse.Success(resp) +} + +func (s *Service) toProtoTransfer(transfer *model.Transfer) *gatewayv1.Transfer { + if transfer == nil { + return nil + } + destination := &gatewayv1.TransferDestination{} + if transfer.Destination.ManagedWalletRef != "" { + destination.Destination = &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: transfer.Destination.ManagedWalletRef} + } else if transfer.Destination.ExternalAddress != "" { + destination.Destination = &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: transfer.Destination.ExternalAddress} + } + destination.Memo = transfer.Destination.Memo + + protoFees := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(transfer.Fees)) + for _, fee := range transfer.Fees { + protoFees = append(protoFees, &gatewayv1.ServiceFeeBreakdown{ + FeeCode: fee.FeeCode, + Amount: cloneMoney(fee.Amount), + Description: fee.Description, + }) + } + + asset := &gatewayv1.Asset{ + Chain: chainEnumFromName(transfer.Network), + TokenSymbol: transfer.TokenSymbol, + ContractAddress: transfer.ContractAddress, + } + + return &gatewayv1.Transfer{ + TransferRef: transfer.TransferRef, + IdempotencyKey: transfer.IdempotencyKey, + OrganizationRef: transfer.OrganizationRef, + SourceWalletRef: transfer.SourceWalletRef, + Destination: destination, + Asset: asset, + RequestedAmount: cloneMoney(transfer.RequestedAmount), + NetAmount: cloneMoney(transfer.NetAmount), + Fees: protoFees, + Status: transferStatusToProto(transfer.Status), + TransactionHash: transfer.TxHash, + FailureReason: transfer.FailureReason, + CreatedAt: timestamppb.New(transfer.CreatedAt.UTC()), + UpdatedAt: timestamppb.New(transfer.UpdatedAt.UTC()), + } +} + +func (s *Service) resolveDestination(ctx context.Context, dest *gatewayv1.TransferDestination, source *model.ManagedWallet) (model.TransferDestination, error) { + if dest == nil { + return model.TransferDestination{}, merrors.InvalidArgument("destination is required") + } + managedRef := strings.TrimSpace(dest.GetManagedWalletRef()) + external := strings.TrimSpace(dest.GetExternalAddress()) + if managedRef != "" && external != "" { + return model.TransferDestination{}, merrors.InvalidArgument("destination must be managed_wallet_ref or external_address") + } + if managedRef != "" { + wallet, err := s.storage.Wallets().Get(ctx, managedRef) + if err != nil { + return model.TransferDestination{}, err + } + if !strings.EqualFold(wallet.Network, source.Network) { + return model.TransferDestination{}, merrors.InvalidArgument("destination wallet network mismatch") + } + if strings.TrimSpace(wallet.DepositAddress) == "" { + return model.TransferDestination{}, merrors.InvalidArgument("destination wallet missing deposit address") + } + return model.TransferDestination{ + ManagedWalletRef: wallet.WalletRef, + Memo: strings.TrimSpace(dest.GetMemo()), + }, nil + } + if external == "" { + return model.TransferDestination{}, merrors.InvalidArgument("destination is required") + } + return model.TransferDestination{ + ExternalAddress: strings.ToLower(external), + Memo: strings.TrimSpace(dest.GetMemo()), + }, nil +} + +func convertFees(fees []*gatewayv1.ServiceFeeBreakdown, currency string) ([]model.ServiceFee, decimal.Decimal, error) { + result := make([]model.ServiceFee, 0, len(fees)) + sum := decimal.NewFromInt(0) + for _, fee := range fees { + if fee == nil || fee.GetAmount() == nil { + return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required") + } + amtCurrency := strings.ToUpper(strings.TrimSpace(fee.GetAmount().GetCurrency())) + if amtCurrency != strings.ToUpper(currency) { + return nil, decimal.Decimal{}, merrors.InvalidArgument("fee currency mismatch") + } + amtValue := strings.TrimSpace(fee.GetAmount().GetAmount()) + if amtValue == "" { + return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount is required") + } + dec, err := decimal.NewFromString(amtValue) + if err != nil { + return nil, decimal.Decimal{}, merrors.InvalidArgument("invalid fee amount") + } + if dec.IsNegative() { + return nil, decimal.Decimal{}, merrors.InvalidArgument("fee amount must be non-negative") + } + sum = sum.Add(dec) + result = append(result, model.ServiceFee{ + FeeCode: strings.TrimSpace(fee.GetFeeCode()), + Amount: cloneMoney(fee.GetAmount()), + Description: strings.TrimSpace(fee.GetDescription()), + }) + } + return result, sum, nil +} diff --git a/api/chain/gateway/internal/service/gateway/wallet_handlers.go b/api/chain/gateway/internal/service/gateway/wallet_handlers.go new file mode 100644 index 0000000..cff2b66 --- /dev/null +++ b/api/chain/gateway/internal/service/gateway/wallet_handlers.go @@ -0,0 +1,213 @@ +package gateway + +import ( + "context" + "errors" + "strings" + + gatewayv1 "github.com/tech/sendico/chain/gateway/internal/generated/service/gateway/v1" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mservice" + paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *Service) createManagedWalletHandler(ctx context.Context, req *gatewayv1.CreateManagedWalletRequest) gsresponse.Responder[gatewayv1.CreateManagedWalletResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err) + } + if req == nil { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request")) + } + + idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey()) + if idempotencyKey == "" { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("idempotency_key is required")) + } + organizationRef := strings.TrimSpace(req.GetOrganizationRef()) + if organizationRef == "" { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("organization_ref is required")) + } + ownerRef := strings.TrimSpace(req.GetOwnerRef()) + if ownerRef == "" { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("owner_ref is required")) + } + + asset := req.GetAsset() + if asset == nil { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset is required")) + } + + chainKey, _ := chainKeyFromEnum(asset.GetChain()) + if chainKey == "" { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain")) + } + networkCfg, ok := s.networks[chainKey] + if !ok { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported chain")) + } + + tokenSymbol := strings.ToUpper(strings.TrimSpace(asset.GetTokenSymbol())) + if tokenSymbol == "" { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("asset.token_symbol is required")) + } + contractAddress := strings.ToLower(strings.TrimSpace(asset.GetContractAddress())) + if contractAddress == "" { + contractAddress = resolveContractAddress(networkCfg.TokenConfigs, tokenSymbol) + if contractAddress == "" { + return gsresponse.InvalidArgument[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("unsupported token for chain")) + } + } + + walletRef := generateWalletRef() + if s.keyManager == nil { + return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager not configured")) + } + + keyInfo, err := s.keyManager.CreateManagedWalletKey(ctx, walletRef, chainKey) + if err != nil { + return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err) + } + if keyInfo == nil || strings.TrimSpace(keyInfo.Address) == "" { + return gsresponse.Internal[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.Internal("key manager returned empty address")) + } + + wallet := &model.ManagedWallet{ + IdempotencyKey: idempotencyKey, + WalletRef: walletRef, + OrganizationRef: organizationRef, + OwnerRef: ownerRef, + Network: chainKey, + TokenSymbol: tokenSymbol, + ContractAddress: contractAddress, + DepositAddress: strings.ToLower(keyInfo.Address), + KeyReference: keyInfo.KeyID, + Status: model.ManagedWalletStatusActive, + Metadata: cloneMetadata(req.GetMetadata()), + } + + created, err := s.storage.Wallets().Create(ctx, wallet) + if err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + s.logger.Debug("wallet already exists", zap.String("wallet_ref", walletRef), zap.String("idempotency_key", idempotencyKey)) + return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)}) + } + return gsresponse.Auto[gatewayv1.CreateManagedWalletResponse](s.logger, mservice.ChainGateway, err) + } + + return gsresponse.Success(&gatewayv1.CreateManagedWalletResponse{Wallet: s.toProtoManagedWallet(created)}) +} + +func (s *Service) getManagedWalletHandler(ctx context.Context, req *gatewayv1.GetManagedWalletRequest) gsresponse.Responder[gatewayv1.GetManagedWalletResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err) + } + if req == nil { + return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request")) + } + walletRef := strings.TrimSpace(req.GetWalletRef()) + if walletRef == "" { + return gsresponse.InvalidArgument[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required")) + } + wallet, err := s.storage.Wallets().Get(ctx, walletRef) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + return gsresponse.NotFound[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Auto[gatewayv1.GetManagedWalletResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Success(&gatewayv1.GetManagedWalletResponse{Wallet: s.toProtoManagedWallet(wallet)}) +} + +func (s *Service) listManagedWalletsHandler(ctx context.Context, req *gatewayv1.ListManagedWalletsRequest) gsresponse.Responder[gatewayv1.ListManagedWalletsResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err) + } + filter := model.ManagedWalletFilter{} + if req != nil { + filter.OrganizationRef = strings.TrimSpace(req.GetOrganizationRef()) + filter.OwnerRef = strings.TrimSpace(req.GetOwnerRef()) + if asset := req.GetAsset(); asset != nil { + filter.Network, _ = chainKeyFromEnum(asset.GetChain()) + filter.TokenSymbol = strings.TrimSpace(asset.GetTokenSymbol()) + } + if page := req.GetPage(); page != nil { + filter.Cursor = strings.TrimSpace(page.GetCursor()) + filter.Limit = page.GetLimit() + } + } + + result, err := s.storage.Wallets().List(ctx, filter) + if err != nil { + return gsresponse.Auto[gatewayv1.ListManagedWalletsResponse](s.logger, mservice.ChainGateway, err) + } + + protoWallets := make([]*gatewayv1.ManagedWallet, 0, len(result.Items)) + for _, wallet := range result.Items { + protoWallets = append(protoWallets, s.toProtoManagedWallet(wallet)) + } + + resp := &gatewayv1.ListManagedWalletsResponse{ + Wallets: protoWallets, + Page: &paginationv1.CursorPageResponse{NextCursor: result.NextCursor}, + } + return gsresponse.Success(resp) +} + +func (s *Service) getWalletBalanceHandler(ctx context.Context, req *gatewayv1.GetWalletBalanceRequest) gsresponse.Responder[gatewayv1.GetWalletBalanceResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err) + } + if req == nil { + return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("nil request")) + } + walletRef := strings.TrimSpace(req.GetWalletRef()) + if walletRef == "" { + return gsresponse.InvalidArgument[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, merrors.InvalidArgument("wallet_ref is required")) + } + balance, err := s.storage.Wallets().GetBalance(ctx, walletRef) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + return gsresponse.NotFound[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Auto[gatewayv1.GetWalletBalanceResponse](s.logger, mservice.ChainGateway, err) + } + return gsresponse.Success(&gatewayv1.GetWalletBalanceResponse{Balance: toProtoWalletBalance(balance)}) +} + +func (s *Service) toProtoManagedWallet(wallet *model.ManagedWallet) *gatewayv1.ManagedWallet { + if wallet == nil { + return nil + } + asset := &gatewayv1.Asset{ + Chain: chainEnumFromName(wallet.Network), + TokenSymbol: wallet.TokenSymbol, + ContractAddress: wallet.ContractAddress, + } + return &gatewayv1.ManagedWallet{ + WalletRef: wallet.WalletRef, + OrganizationRef: wallet.OrganizationRef, + OwnerRef: wallet.OwnerRef, + Asset: asset, + DepositAddress: wallet.DepositAddress, + Status: managedWalletStatusToProto(wallet.Status), + Metadata: cloneMetadata(wallet.Metadata), + CreatedAt: timestamppb.New(wallet.CreatedAt.UTC()), + UpdatedAt: timestamppb.New(wallet.UpdatedAt.UTC()), + } +} + +func toProtoWalletBalance(balance *model.WalletBalance) *gatewayv1.WalletBalance { + if balance == nil { + return nil + } + return &gatewayv1.WalletBalance{ + Available: cloneMoney(balance.Available), + PendingInbound: cloneMoney(balance.PendingInbound), + PendingOutbound: cloneMoney(balance.PendingOutbound), + CalculatedAt: timestamppb.New(balance.CalculatedAt.UTC()), + } +} diff --git a/api/chain/gateway/main.go b/api/chain/gateway/main.go new file mode 100644 index 0000000..fe8b96a --- /dev/null +++ b/api/chain/gateway/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/tech/sendico/chain/gateway/internal/appversion" + si "github.com/tech/sendico/chain/gateway/internal/server" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" + smain "github.com/tech/sendico/pkg/server/main" +) + +func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return si.Create(logger, file, debug) +} + +func main() { + smain.RunServer("main", appversion.Create(), factory) +} diff --git a/api/chain/gateway/storage/model/deposit.go b/api/chain/gateway/storage/model/deposit.go new file mode 100644 index 0000000..64db4ea --- /dev/null +++ b/api/chain/gateway/storage/model/deposit.go @@ -0,0 +1,54 @@ +package model + +import ( + "strings" + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/mservice" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" +) + +type DepositStatus string + +const ( + DepositStatusPending DepositStatus = "pending" + DepositStatusConfirmed DepositStatus = "confirmed" + DepositStatusFailed DepositStatus = "failed" +) + +// Deposit records an inbound transfer observed on-chain. +type Deposit struct { + storable.Base `bson:",inline" json:",inline"` + + DepositRef string `bson:"depositRef" json:"depositRef"` + WalletRef string `bson:"walletRef" json:"walletRef"` + Network string `bson:"network" json:"network"` + TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"` + ContractAddress string `bson:"contractAddress" json:"contractAddress"` + Amount *moneyv1.Money `bson:"amount" json:"amount"` + SourceAddress string `bson:"sourceAddress" json:"sourceAddress"` + TxHash string `bson:"txHash" json:"txHash"` + BlockID string `bson:"blockId,omitempty" json:"blockId,omitempty"` + Status DepositStatus `bson:"status" json:"status"` + ObservedAt time.Time `bson:"observedAt" json:"observedAt"` + RecordedAt time.Time `bson:"recordedAt" json:"recordedAt"` + LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"` +} + +// Collection implements storable.Storable. +func (*Deposit) Collection() string { + return mservice.ChainDeposits +} + +// Normalize standardizes case-sensitive fields. +func (d *Deposit) Normalize() { + d.DepositRef = strings.TrimSpace(d.DepositRef) + d.WalletRef = strings.TrimSpace(d.WalletRef) + d.Network = strings.TrimSpace(strings.ToLower(d.Network)) + d.TokenSymbol = strings.TrimSpace(strings.ToUpper(d.TokenSymbol)) + d.ContractAddress = strings.TrimSpace(strings.ToLower(d.ContractAddress)) + d.SourceAddress = strings.TrimSpace(strings.ToLower(d.SourceAddress)) + d.TxHash = strings.TrimSpace(strings.ToLower(d.TxHash)) + d.BlockID = strings.TrimSpace(d.BlockID) +} diff --git a/api/chain/gateway/storage/model/transfer.go b/api/chain/gateway/storage/model/transfer.go new file mode 100644 index 0000000..1ee246b --- /dev/null +++ b/api/chain/gateway/storage/model/transfer.go @@ -0,0 +1,91 @@ +package model + +import ( + "strings" + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/mservice" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" +) + +type TransferStatus string + +const ( + TransferStatusPending TransferStatus = "pending" + TransferStatusSigning TransferStatus = "signing" + TransferStatusSubmitted TransferStatus = "submitted" + TransferStatusConfirmed TransferStatus = "confirmed" + TransferStatusFailed TransferStatus = "failed" + TransferStatusCancelled TransferStatus = "cancelled" +) + +// ServiceFee represents a fee component applied to a transfer. +type ServiceFee struct { + FeeCode string `bson:"feeCode" json:"feeCode"` + Amount *moneyv1.Money `bson:"amount" json:"amount"` + Description string `bson:"description,omitempty" json:"description,omitempty"` +} + +type TransferDestination struct { + ManagedWalletRef string `bson:"managedWalletRef,omitempty" json:"managedWalletRef,omitempty"` + ExternalAddress string `bson:"externalAddress,omitempty" json:"externalAddress,omitempty"` + Memo string `bson:"memo,omitempty" json:"memo,omitempty"` +} + +// Transfer models an on-chain transfer orchestrated by the gateway. +type Transfer struct { + storable.Base `bson:",inline" json:",inline"` + + TransferRef string `bson:"transferRef" json:"transferRef"` + IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"` + OrganizationRef string `bson:"organizationRef" json:"organizationRef"` + SourceWalletRef string `bson:"sourceWalletRef" json:"sourceWalletRef"` + Destination TransferDestination `bson:"destination" json:"destination"` + Network string `bson:"network" json:"network"` + TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"` + ContractAddress string `bson:"contractAddress" json:"contractAddress"` + RequestedAmount *moneyv1.Money `bson:"requestedAmount" json:"requestedAmount"` + NetAmount *moneyv1.Money `bson:"netAmount" json:"netAmount"` + Fees []ServiceFee `bson:"fees,omitempty" json:"fees,omitempty"` + Status TransferStatus `bson:"status" json:"status"` + TxHash string `bson:"txHash,omitempty" json:"txHash,omitempty"` + FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"` + ClientReference string `bson:"clientReference,omitempty" json:"clientReference,omitempty"` + LastStatusAt time.Time `bson:"lastStatusAt" json:"lastStatusAt"` +} + +// Collection implements storable.Storable. +func (*Transfer) Collection() string { + return mservice.ChainTransfers +} + +// TransferFilter describes the parameters for listing transfers. +type TransferFilter struct { + SourceWalletRef string + DestinationWalletRef string + Status TransferStatus + Cursor string + Limit int32 +} + +// TransferList contains paginated transfer results. +type TransferList struct { + Items []*Transfer + NextCursor string +} + +// Normalize trims strings for consistent indexes. +func (t *Transfer) Normalize() { + t.TransferRef = strings.TrimSpace(t.TransferRef) + t.IdempotencyKey = strings.TrimSpace(t.IdempotencyKey) + t.OrganizationRef = strings.TrimSpace(t.OrganizationRef) + t.SourceWalletRef = strings.TrimSpace(t.SourceWalletRef) + t.Network = strings.TrimSpace(strings.ToLower(t.Network)) + t.TokenSymbol = strings.TrimSpace(strings.ToUpper(t.TokenSymbol)) + t.ContractAddress = strings.TrimSpace(strings.ToLower(t.ContractAddress)) + t.Destination.ManagedWalletRef = strings.TrimSpace(t.Destination.ManagedWalletRef) + t.Destination.ExternalAddress = strings.TrimSpace(strings.ToLower(t.Destination.ExternalAddress)) + t.Destination.Memo = strings.TrimSpace(t.Destination.Memo) + t.ClientReference = strings.TrimSpace(t.ClientReference) +} diff --git a/api/chain/gateway/storage/model/wallet.go b/api/chain/gateway/storage/model/wallet.go new file mode 100644 index 0000000..7080b26 --- /dev/null +++ b/api/chain/gateway/storage/model/wallet.go @@ -0,0 +1,90 @@ +package model + +import ( + "strings" + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/mservice" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" +) + +type ManagedWalletStatus string + +const ( + ManagedWalletStatusActive ManagedWalletStatus = "active" + ManagedWalletStatusSuspended ManagedWalletStatus = "suspended" + ManagedWalletStatusClosed ManagedWalletStatus = "closed" +) + +// ManagedWallet represents a user-controlled on-chain wallet managed by the service. +type ManagedWallet struct { + storable.Base `bson:",inline" json:",inline"` + + IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"` + WalletRef string `bson:"walletRef" json:"walletRef"` + OrganizationRef string `bson:"organizationRef" json:"organizationRef"` + OwnerRef string `bson:"ownerRef" json:"ownerRef"` + Network string `bson:"network" json:"network"` + TokenSymbol string `bson:"tokenSymbol" json:"tokenSymbol"` + ContractAddress string `bson:"contractAddress" json:"contractAddress"` + DepositAddress string `bson:"depositAddress" json:"depositAddress"` + KeyReference string `bson:"keyReference,omitempty" json:"keyReference,omitempty"` + Status ManagedWalletStatus `bson:"status" json:"status"` + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// Collection implements storable.Storable. +func (*ManagedWallet) Collection() string { + return mservice.ChainWallets +} + +// WalletBalance captures computed wallet balances. +type WalletBalance struct { + storable.Base `bson:",inline" json:",inline"` + + WalletRef string `bson:"walletRef" json:"walletRef"` + Available *moneyv1.Money `bson:"available" json:"available"` + PendingInbound *moneyv1.Money `bson:"pendingInbound,omitempty" json:"pendingInbound,omitempty"` + PendingOutbound *moneyv1.Money `bson:"pendingOutbound,omitempty" json:"pendingOutbound,omitempty"` + CalculatedAt time.Time `bson:"calculatedAt" json:"calculatedAt"` +} + +// Collection implements storable.Storable. +func (*WalletBalance) Collection() string { + return mservice.ChainWalletBalances +} + +// ManagedWalletFilter describes list filters. +type ManagedWalletFilter struct { + OrganizationRef string + OwnerRef string + Network string + TokenSymbol string + Cursor string + Limit int32 +} + +// ManagedWalletList contains paginated wallet results. +type ManagedWalletList struct { + Items []*ManagedWallet + NextCursor string +} + +// Normalize trims string fields for consistent indexing. +func (m *ManagedWallet) Normalize() { + m.IdempotencyKey = strings.TrimSpace(m.IdempotencyKey) + m.WalletRef = strings.TrimSpace(m.WalletRef) + m.OrganizationRef = strings.TrimSpace(m.OrganizationRef) + m.OwnerRef = strings.TrimSpace(m.OwnerRef) + m.Network = strings.TrimSpace(strings.ToLower(m.Network)) + m.TokenSymbol = strings.TrimSpace(strings.ToUpper(m.TokenSymbol)) + m.ContractAddress = strings.TrimSpace(strings.ToLower(m.ContractAddress)) + m.DepositAddress = strings.TrimSpace(strings.ToLower(m.DepositAddress)) + m.KeyReference = strings.TrimSpace(m.KeyReference) +} + +// Normalize trims wallet balance identifiers. +func (b *WalletBalance) Normalize() { + b.WalletRef = strings.TrimSpace(b.WalletRef) +} diff --git a/api/chain/gateway/storage/mongo/repository.go b/api/chain/gateway/storage/mongo/repository.go new file mode 100644 index 0000000..0bc4be8 --- /dev/null +++ b/api/chain/gateway/storage/mongo/repository.go @@ -0,0 +1,98 @@ +package mongo + +import ( + "context" + "time" + + "github.com/tech/sendico/chain/gateway/storage" + "github.com/tech/sendico/chain/gateway/storage/mongo/store" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +// Store implements storage.Repository backed by MongoDB. +type Store struct { + logger mlogger.Logger + conn *db.MongoConnection + db *mongo.Database + + wallets storage.WalletsStore + transfers storage.TransfersStore + deposits storage.DepositsStore +} + +// New creates a new Mongo-backed repository. +func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) { + if conn == nil { + return nil, merrors.InvalidArgument("mongo connection is nil") + } + client := conn.Client() + if client == nil { + return nil, merrors.Internal("mongo client is not initialised") + } + + result := &Store{ + logger: logger.Named("storage").Named("mongo"), + conn: conn, + db: conn.Database(), + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := result.Ping(ctx); err != nil { + result.logger.Error("mongo ping failed during repository initialisation", zap.Error(err)) + return nil, err + } + + walletsStore, err := store.NewWallets(result.logger, result.db) + if err != nil { + result.logger.Error("failed to initialise wallets store", zap.Error(err)) + return nil, err + } + transfersStore, err := store.NewTransfers(result.logger, result.db) + if err != nil { + result.logger.Error("failed to initialise transfers store", zap.Error(err)) + return nil, err + } + depositsStore, err := store.NewDeposits(result.logger, result.db) + if err != nil { + result.logger.Error("failed to initialise deposits store", zap.Error(err)) + return nil, err + } + + result.wallets = walletsStore + result.transfers = transfersStore + result.deposits = depositsStore + + result.logger.Info("Chain gateway MongoDB storage initialised") + return result, nil +} + +// Ping verifies the MongoDB connection. +func (s *Store) Ping(ctx context.Context) error { + if s.conn == nil { + return merrors.InvalidArgument("mongo connection is nil") + } + return s.conn.Ping(ctx) +} + +// Wallets returns the wallets store. +func (s *Store) Wallets() storage.WalletsStore { + return s.wallets +} + +// Transfers returns the transfers store. +func (s *Store) Transfers() storage.TransfersStore { + return s.transfers +} + +// Deposits returns the deposits store. +func (s *Store) Deposits() storage.DepositsStore { + return s.deposits +} + +var _ storage.Repository = (*Store)(nil) diff --git a/api/chain/gateway/storage/mongo/store/deposits.go b/api/chain/gateway/storage/mongo/store/deposits.go new file mode 100644 index 0000000..a3f0f5c --- /dev/null +++ b/api/chain/gateway/storage/mongo/store/deposits.go @@ -0,0 +1,161 @@ +package store + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/tech/sendico/chain/gateway/storage" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +const ( + defaultDepositPageSize int64 = 100 + maxDepositPageSize int64 = 500 +) + +type Deposits struct { + logger mlogger.Logger + repo repository.Repository +} + +// NewDeposits constructs a Mongo-backed deposits store. +func NewDeposits(logger mlogger.Logger, db *mongo.Database) (*Deposits, error) { + if db == nil { + return nil, merrors.InvalidArgument("mongo database is nil") + } + repo := repository.CreateMongoRepository(db, mservice.ChainDeposits) + indexes := []*ri.Definition{ + { + Keys: []ri.Key{{Field: "depositRef", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}, {Field: "status", Sort: ri.Asc}}, + }, + { + Keys: []ri.Key{{Field: "txHash", Sort: ri.Asc}}, + Unique: true, + }, + } + for _, def := range indexes { + if err := repo.CreateIndex(def); err != nil { + logger.Error("failed to ensure deposit index", zap.Error(err), zap.String("collection", repo.Collection())) + return nil, err + } + } + + childLogger := logger.Named("deposits") + childLogger.Debug("deposits store initialised") + + return &Deposits{logger: childLogger, repo: repo}, nil +} + +func (d *Deposits) Record(ctx context.Context, deposit *model.Deposit) error { + if deposit == nil { + return merrors.InvalidArgument("depositsStore: nil deposit") + } + deposit.Normalize() + if strings.TrimSpace(deposit.DepositRef) == "" { + return merrors.InvalidArgument("depositsStore: empty depositRef") + } + if deposit.Status == "" { + deposit.Status = model.DepositStatusPending + } + if deposit.ObservedAt.IsZero() { + deposit.ObservedAt = time.Now().UTC() + } + if deposit.RecordedAt.IsZero() { + deposit.RecordedAt = time.Now().UTC() + } + if deposit.LastStatusAt.IsZero() { + deposit.LastStatusAt = time.Now().UTC() + } + + existing := &model.Deposit{} + err := d.repo.FindOneByFilter(ctx, repository.Filter("depositRef", deposit.DepositRef), existing) + switch { + case err == nil: + existing.Status = deposit.Status + existing.ObservedAt = deposit.ObservedAt + existing.RecordedAt = deposit.RecordedAt + existing.LastStatusAt = time.Now().UTC() + if deposit.Amount != nil { + existing.Amount = deposit.Amount + } + if deposit.BlockID != "" { + existing.BlockID = deposit.BlockID + } + if deposit.TxHash != "" { + existing.TxHash = deposit.TxHash + } + if deposit.Network != "" { + existing.Network = deposit.Network + } + if deposit.TokenSymbol != "" { + existing.TokenSymbol = deposit.TokenSymbol + } + if deposit.ContractAddress != "" { + existing.ContractAddress = deposit.ContractAddress + } + if deposit.SourceAddress != "" { + existing.SourceAddress = deposit.SourceAddress + } + if err := d.repo.Update(ctx, existing); err != nil { + return err + } + return nil + case errors.Is(err, merrors.ErrNoData): + if err := d.repo.Insert(ctx, deposit, repository.Filter("depositRef", deposit.DepositRef)); err != nil { + return err + } + return nil + default: + return err + } +} + +func (d *Deposits) ListPending(ctx context.Context, network string, limit int32) ([]*model.Deposit, error) { + query := repository.Query().Filter(repository.Field("status"), model.DepositStatusPending) + if net := strings.TrimSpace(network); net != "" { + query = query.Filter(repository.Field("network"), strings.ToLower(net)) + } + pageSize := sanitizeDepositLimit(limit) + query = query.Sort(repository.Field("observedAt"), true).Limit(&pageSize) + + deposits := make([]*model.Deposit, 0, pageSize) + decoder := func(cur *mongo.Cursor) error { + item := &model.Deposit{} + if err := cur.Decode(item); err != nil { + return err + } + deposits = append(deposits, item) + return nil + } + + if err := d.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) { + return nil, err + } + + return deposits, nil +} + +func sanitizeDepositLimit(requested int32) int64 { + if requested <= 0 { + return defaultDepositPageSize + } + if requested > int32(maxDepositPageSize) { + return maxDepositPageSize + } + return int64(requested) +} + +var _ storage.DepositsStore = (*Deposits)(nil) diff --git a/api/chain/gateway/storage/mongo/store/transfers.go b/api/chain/gateway/storage/mongo/store/transfers.go new file mode 100644 index 0000000..d4d457c --- /dev/null +++ b/api/chain/gateway/storage/mongo/store/transfers.go @@ -0,0 +1,200 @@ +package store + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/tech/sendico/chain/gateway/storage" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +const ( + defaultTransferPageSize int64 = 50 + maxTransferPageSize int64 = 200 +) + +type Transfers struct { + logger mlogger.Logger + repo repository.Repository +} + +// NewTransfers constructs a Mongo-backed transfers store. +func NewTransfers(logger mlogger.Logger, db *mongo.Database) (*Transfers, error) { + if db == nil { + return nil, merrors.InvalidArgument("mongo database is nil") + } + repo := repository.CreateMongoRepository(db, mservice.ChainTransfers) + indexes := []*ri.Definition{ + { + Keys: []ri.Key{{Field: "transferRef", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "sourceWalletRef", Sort: ri.Asc}, {Field: "status", Sort: ri.Asc}}, + }, + { + Keys: []ri.Key{{Field: "destination.managedWalletRef", Sort: ri.Asc}}, + }, + } + for _, def := range indexes { + if err := repo.CreateIndex(def); err != nil { + logger.Error("failed to ensure transfer index", zap.Error(err), zap.String("collection", repo.Collection())) + return nil, err + } + } + + childLogger := logger.Named("transfers") + childLogger.Debug("transfers store initialised") + + return &Transfers{ + logger: childLogger, + repo: repo, + }, nil +} + +func (t *Transfers) Create(ctx context.Context, transfer *model.Transfer) (*model.Transfer, error) { + if transfer == nil { + return nil, merrors.InvalidArgument("transfersStore: nil transfer") + } + transfer.Normalize() + if strings.TrimSpace(transfer.TransferRef) == "" { + return nil, merrors.InvalidArgument("transfersStore: empty transferRef") + } + if strings.TrimSpace(transfer.IdempotencyKey) == "" { + return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey") + } + if transfer.Status == "" { + transfer.Status = model.TransferStatusPending + } + if transfer.LastStatusAt.IsZero() { + transfer.LastStatusAt = time.Now().UTC() + } + if strings.TrimSpace(transfer.IdempotencyKey) == "" { + return nil, merrors.InvalidArgument("transfersStore: empty idempotencyKey") + } + if err := t.repo.Insert(ctx, transfer, repository.Filter("idempotencyKey", transfer.IdempotencyKey)); err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + t.logger.Debug("transfer already exists", zap.String("transfer_ref", transfer.TransferRef), zap.String("idempotency_key", transfer.IdempotencyKey)) + return transfer, nil + } + return nil, err + } + t.logger.Debug("transfer created", zap.String("transfer_ref", transfer.TransferRef)) + return transfer, nil +} + +func (t *Transfers) Get(ctx context.Context, transferRef string) (*model.Transfer, error) { + transferRef = strings.TrimSpace(transferRef) + if transferRef == "" { + return nil, merrors.InvalidArgument("transfersStore: empty transferRef") + } + transfer := &model.Transfer{} + if err := t.repo.FindOneByFilter(ctx, repository.Filter("transferRef", transferRef), transfer); err != nil { + return nil, err + } + return transfer, nil +} + +func (t *Transfers) List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) { + query := repository.Query() + if src := strings.TrimSpace(filter.SourceWalletRef); src != "" { + query = query.Filter(repository.Field("sourceWalletRef"), src) + } + if dst := strings.TrimSpace(filter.DestinationWalletRef); dst != "" { + query = query.Filter(repository.Field("destination.managedWalletRef"), dst) + } + if status := strings.TrimSpace(string(filter.Status)); status != "" { + query = query.Filter(repository.Field("status"), status) + } + + if cursor := strings.TrimSpace(filter.Cursor); cursor != "" { + if oid, err := primitive.ObjectIDFromHex(cursor); err == nil { + query = query.Comparison(repository.IDField(), builder.Gt, oid) + } else { + t.logger.Warn("ignoring invalid transfer cursor", zap.String("cursor", cursor), zap.Error(err)) + } + } + + limit := sanitizeTransferLimit(filter.Limit) + fetchLimit := limit + 1 + query = query.Sort(repository.IDField(), true).Limit(&fetchLimit) + + transfers := make([]*model.Transfer, 0, fetchLimit) + decoder := func(cur *mongo.Cursor) error { + item := &model.Transfer{} + if err := cur.Decode(item); err != nil { + return err + } + transfers = append(transfers, item) + return nil + } + + if err := t.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) { + return nil, err + } + + nextCursor := "" + if int64(len(transfers)) == fetchLimit { + last := transfers[len(transfers)-1] + nextCursor = last.ID.Hex() + transfers = transfers[:len(transfers)-1] + } + + return &model.TransferList{ + Items: transfers, + NextCursor: nextCursor, + }, nil +} + +func (t *Transfers) UpdateStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason string, txHash string) (*model.Transfer, error) { + transferRef = strings.TrimSpace(transferRef) + if transferRef == "" { + return nil, merrors.InvalidArgument("transfersStore: empty transferRef") + } + transfer := &model.Transfer{} + if err := t.repo.FindOneByFilter(ctx, repository.Filter("transferRef", transferRef), transfer); err != nil { + return nil, err + } + + transfer.Status = status + if status == model.TransferStatusFailed { + transfer.FailureReason = strings.TrimSpace(failureReason) + } else { + transfer.FailureReason = "" + } + if hash := strings.TrimSpace(txHash); hash != "" { + transfer.TxHash = strings.ToLower(hash) + } + transfer.LastStatusAt = time.Now().UTC() + if err := t.repo.Update(ctx, transfer); err != nil { + return nil, err + } + return transfer, nil +} + +func sanitizeTransferLimit(requested int32) int64 { + if requested <= 0 { + return defaultTransferPageSize + } + if requested > int32(maxTransferPageSize) { + return maxTransferPageSize + } + return int64(requested) +} + +var _ storage.TransfersStore = (*Transfers)(nil) diff --git a/api/chain/gateway/storage/mongo/store/wallets.go b/api/chain/gateway/storage/mongo/store/wallets.go new file mode 100644 index 0000000..c3f3b07 --- /dev/null +++ b/api/chain/gateway/storage/mongo/store/wallets.go @@ -0,0 +1,236 @@ +package store + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/tech/sendico/chain/gateway/storage" + "github.com/tech/sendico/chain/gateway/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +const ( + defaultWalletPageSize int64 = 50 + maxWalletPageSize int64 = 200 +) + +type Wallets struct { + logger mlogger.Logger + walletRepo repository.Repository + balanceRepo repository.Repository +} + +// NewWallets constructs a Mongo-backed wallets store. +func NewWallets(logger mlogger.Logger, db *mongo.Database) (*Wallets, error) { + if db == nil { + return nil, merrors.InvalidArgument("mongo database is nil") + } + + walletRepo := repository.CreateMongoRepository(db, mservice.ChainWallets) + walletIndexes := []*ri.Definition{ + { + Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "depositAddress", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "organizationRef", Sort: ri.Asc}, {Field: "ownerRef", Sort: ri.Asc}}, + }, + } + for _, def := range walletIndexes { + if err := walletRepo.CreateIndex(def); err != nil { + logger.Error("failed to ensure wallet index", zap.String("collection", walletRepo.Collection()), zap.Error(err)) + return nil, err + } + } + + balanceRepo := repository.CreateMongoRepository(db, mservice.ChainWalletBalances) + balanceIndexes := []*ri.Definition{ + { + Keys: []ri.Key{{Field: "walletRef", Sort: ri.Asc}}, + Unique: true, + }, + } + for _, def := range balanceIndexes { + if err := balanceRepo.CreateIndex(def); err != nil { + logger.Error("failed to ensure wallet balance index", zap.String("collection", balanceRepo.Collection()), zap.Error(err)) + return nil, err + } + } + + childLogger := logger.Named("wallets") + childLogger.Debug("wallet stores initialised") + + return &Wallets{ + logger: childLogger, + walletRepo: walletRepo, + balanceRepo: balanceRepo, + }, nil +} + +func (w *Wallets) Create(ctx context.Context, wallet *model.ManagedWallet) (*model.ManagedWallet, error) { + if wallet == nil { + return nil, merrors.InvalidArgument("walletsStore: nil wallet") + } + wallet.Normalize() + if strings.TrimSpace(wallet.WalletRef) == "" { + return nil, merrors.InvalidArgument("walletsStore: empty walletRef") + } + if wallet.Status == "" { + wallet.Status = model.ManagedWalletStatusActive + } + if strings.TrimSpace(wallet.IdempotencyKey) == "" { + return nil, merrors.InvalidArgument("walletsStore: empty idempotencyKey") + } + if err := w.walletRepo.Insert(ctx, wallet, repository.Filter("idempotencyKey", wallet.IdempotencyKey)); err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + w.logger.Debug("wallet already exists", zap.String("wallet_ref", wallet.WalletRef), zap.String("idempotency_key", wallet.IdempotencyKey)) + return wallet, nil + } + return nil, err + } + w.logger.Debug("wallet created", zap.String("wallet_ref", wallet.WalletRef)) + return wallet, nil +} + +func (w *Wallets) Get(ctx context.Context, walletRef string) (*model.ManagedWallet, error) { + walletRef = strings.TrimSpace(walletRef) + if walletRef == "" { + return nil, merrors.InvalidArgument("walletsStore: empty walletRef") + } + wallet := &model.ManagedWallet{} + if err := w.walletRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletRef), wallet); err != nil { + return nil, err + } + return wallet, nil +} + +func (w *Wallets) List(ctx context.Context, filter model.ManagedWalletFilter) (*model.ManagedWalletList, error) { + query := repository.Query() + + if org := strings.TrimSpace(filter.OrganizationRef); org != "" { + query = query.Filter(repository.Field("organizationRef"), org) + } + if owner := strings.TrimSpace(filter.OwnerRef); owner != "" { + query = query.Filter(repository.Field("ownerRef"), owner) + } + if network := strings.TrimSpace(filter.Network); network != "" { + query = query.Filter(repository.Field("network"), strings.ToLower(network)) + } + if token := strings.TrimSpace(filter.TokenSymbol); token != "" { + query = query.Filter(repository.Field("tokenSymbol"), strings.ToUpper(token)) + } + + if cursor := strings.TrimSpace(filter.Cursor); cursor != "" { + if oid, err := primitive.ObjectIDFromHex(cursor); err == nil { + query = query.Comparison(repository.IDField(), builder.Gt, oid) + } else { + w.logger.Warn("ignoring invalid wallet cursor", zap.String("cursor", cursor), zap.Error(err)) + } + } + + limit := sanitizeWalletLimit(filter.Limit) + fetchLimit := limit + 1 + query = query.Sort(repository.IDField(), true).Limit(&fetchLimit) + + wallets := make([]*model.ManagedWallet, 0, fetchLimit) + decoder := func(cur *mongo.Cursor) error { + item := &model.ManagedWallet{} + if err := cur.Decode(item); err != nil { + return err + } + wallets = append(wallets, item) + return nil + } + + if err := w.walletRepo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) { + return nil, err + } + + nextCursor := "" + if int64(len(wallets)) == fetchLimit { + last := wallets[len(wallets)-1] + nextCursor = last.ID.Hex() + wallets = wallets[:len(wallets)-1] + } + + return &model.ManagedWalletList{ + Items: wallets, + NextCursor: nextCursor, + }, nil +} + +func (w *Wallets) SaveBalance(ctx context.Context, balance *model.WalletBalance) error { + if balance == nil { + return merrors.InvalidArgument("walletsStore: nil balance") + } + balance.Normalize() + if strings.TrimSpace(balance.WalletRef) == "" { + return merrors.InvalidArgument("walletsStore: empty walletRef for balance") + } + if balance.CalculatedAt.IsZero() { + balance.CalculatedAt = time.Now().UTC() + } + + existing := &model.WalletBalance{} + err := w.balanceRepo.FindOneByFilter(ctx, repository.Filter("walletRef", balance.WalletRef), existing) + switch { + case err == nil: + existing.Available = balance.Available + existing.PendingInbound = balance.PendingInbound + existing.PendingOutbound = balance.PendingOutbound + existing.CalculatedAt = balance.CalculatedAt + if err := w.balanceRepo.Update(ctx, existing); err != nil { + return err + } + return nil + case errors.Is(err, merrors.ErrNoData): + if err := w.balanceRepo.Insert(ctx, balance, repository.Filter("walletRef", balance.WalletRef)); err != nil { + return err + } + return nil + default: + return err + } +} + +func (w *Wallets) GetBalance(ctx context.Context, walletRef string) (*model.WalletBalance, error) { + walletRef = strings.TrimSpace(walletRef) + if walletRef == "" { + return nil, merrors.InvalidArgument("walletsStore: empty walletRef") + } + balance := &model.WalletBalance{} + if err := w.balanceRepo.FindOneByFilter(ctx, repository.Filter("walletRef", walletRef), balance); err != nil { + return nil, err + } + return balance, nil +} + +func sanitizeWalletLimit(requested int32) int64 { + if requested <= 0 { + return defaultWalletPageSize + } + if requested > int32(maxWalletPageSize) { + return maxWalletPageSize + } + return int64(requested) +} + +var _ storage.WalletsStore = (*Wallets)(nil) diff --git a/api/chain/gateway/storage/storage.go b/api/chain/gateway/storage/storage.go new file mode 100644 index 0000000..fc3c19a --- /dev/null +++ b/api/chain/gateway/storage/storage.go @@ -0,0 +1,53 @@ +package storage + +import ( + "context" + + "github.com/tech/sendico/chain/gateway/storage/model" +) + +type storageError string + +func (e storageError) Error() string { + return string(e) +} + +var ( + // ErrWalletNotFound indicates that a wallet record was not found. + ErrWalletNotFound = storageError("chain.gateway.storage: wallet not found") + // ErrTransferNotFound indicates that a transfer record was not found. + ErrTransferNotFound = storageError("chain.gateway.storage: transfer not found") + // ErrDepositNotFound indicates that a deposit record was not found. + ErrDepositNotFound = storageError("chain.gateway.storage: deposit not found") +) + +// Repository represents the root storage contract for the chain gateway module. +type Repository interface { + Ping(ctx context.Context) error + Wallets() WalletsStore + Transfers() TransfersStore + Deposits() DepositsStore +} + +// WalletsStore exposes persistence operations for managed wallets. +type WalletsStore interface { + Create(ctx context.Context, wallet *model.ManagedWallet) (*model.ManagedWallet, error) + Get(ctx context.Context, walletRef string) (*model.ManagedWallet, error) + List(ctx context.Context, filter model.ManagedWalletFilter) (*model.ManagedWalletList, error) + SaveBalance(ctx context.Context, balance *model.WalletBalance) error + GetBalance(ctx context.Context, walletRef string) (*model.WalletBalance, error) +} + +// TransfersStore exposes persistence operations for transfers. +type TransfersStore interface { + Create(ctx context.Context, transfer *model.Transfer) (*model.Transfer, error) + Get(ctx context.Context, transferRef string) (*model.Transfer, error) + List(ctx context.Context, filter model.TransferFilter) (*model.TransferList, error) + UpdateStatus(ctx context.Context, transferRef string, status model.TransferStatus, failureReason string, txHash string) (*model.Transfer, error) +} + +// DepositsStore exposes persistence operations for observed deposits. +type DepositsStore interface { + Record(ctx context.Context, deposit *model.Deposit) error + ListPending(ctx context.Context, network string, limit int32) ([]*model.Deposit, error) +} diff --git a/api/fx/ingestor/.DS_Store b/api/fx/ingestor/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..ffdd74964238c5838f4f46186fb157398a126339 GIT binary patch literal 6148 zcmeHKO-lnY5Ph*d6uk84G5=uk;(00StrS7Lt8LX5>Mr{M9`n zVzh#dDQIgtclmsMu7E4x3b+D)PywFVVv9>f?_B{`z!msbK=y~oCRiFK#k_T}%U1y6 zoat=zr%}X_ypc7RhHi(SHQ8Al|tGzo5V;NbW47 literal 0 HcmV?d00001 diff --git a/api/fx/ingestor/.air.toml b/api/fx/ingestor/.air.toml new file mode 100644 index 0000000..f78a7e9 --- /dev/null +++ b/api/fx/ingestor/.air.toml @@ -0,0 +1,32 @@ +# Config file for Air in TOML format + +root = "./../.." +tmp_dir = "tmp" + +[build] +cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/fx/ingestor/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/fx/ingestor/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/fx/ingestor/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/fx/ingestor/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/fx/ingestor/internal/appversion.BuildDate=$(date)'\"" +bin = "./app" +full_bin = "./app --debug --config.file=config.yml" +include_ext = ["go", "yaml", "yml"] +exclude_dir = ["fx/ingestor/tmp", "pkg/.git", "fx/ingestor/env"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true +follow_symlink = true +log = "air.log" +delay = 0 +stop_on_error = true +send_interrupt = true +kill_delay = 500 +args_bin = [] + +[log] +time = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true diff --git a/api/fx/ingestor/.gitignore b/api/fx/ingestor/.gitignore new file mode 100644 index 0000000..dc67a7e --- /dev/null +++ b/api/fx/ingestor/.gitignore @@ -0,0 +1,3 @@ +internal/generated +.gocache +app \ No newline at end of file diff --git a/api/fx/ingestor/config.yml b/api/fx/ingestor/config.yml new file mode 100644 index 0000000..df025dc --- /dev/null +++ b/api/fx/ingestor/config.yml @@ -0,0 +1,43 @@ +poll_interval_seconds: 30 + +market: + sources: + - driver: BINANCE + settings: + base_url: "https://api.binance.com" + - driver: COINGECKO + settings: + base_url: "https://api.coingecko.com/api/v3" + pairs: + BINANCE: + - base: "USDT" + quote: "EUR" + symbol: "EURUSDT" + invert: true + - base: "UAH" + quote: "USDT" + symbol: "USDTUAH" + invert: true + - base: "USDC" + quote: "EUR" + symbol: "EURUSDC" + invert: true + COINGECKO: + - base: "USDT" + quote: "RUB" + symbol: "tether:rub" + +metrics: + enabled: true + address: ":9102" + +database: + driver: mongodb + settings: + host_env: FX_MONGO_HOST + port_env: FX_MONGO_PORT + database_env: FX_MONGO_DATABASE + user_env: FX_MONGO_USER + password_env: FX_MONGO_PASSWORD + auth_source_env: FX_MONGO_AUTH_SOURCE + replica_set_env: FX_MONGO_REPLICA_SET diff --git a/api/fx/ingestor/env/.gitignore b/api/fx/ingestor/env/.gitignore new file mode 100644 index 0000000..d71ab6c --- /dev/null +++ b/api/fx/ingestor/env/.gitignore @@ -0,0 +1 @@ +.env.api \ No newline at end of file diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod new file mode 100644 index 0000000..958e5fe --- /dev/null +++ b/api/fx/ingestor/go.mod @@ -0,0 +1,55 @@ +module github.com/tech/sendico/fx/ingestor + +go 1.25.3 + +replace github.com/tech/sendico/pkg => ../../pkg + +replace github.com/tech/sendico/fx/storage => ../storage + +require ( + github.com/go-chi/chi/v5 v5.2.3 + github.com/google/go-cmp v0.7.0 + github.com/prometheus/client_golang v1.23.2 + github.com/tech/sendico/fx/storage v0.0.0 + github.com/tech/sendico/pkg v0.1.0 + go.uber.org/zap v1.27.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.mongodb.org/mongo-driver v1.17.6 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect + google.golang.org/grpc v1.76.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum new file mode 100644 index 0000000..1558ea2 --- /dev/null +++ b/api/fx/ingestor/go.sum @@ -0,0 +1,225 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= +github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +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/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +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/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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/fx/ingestor/internal/appversion/version.go b/api/fx/ingestor/internal/appversion/version.go new file mode 100644 index 0000000..d947434 --- /dev/null +++ b/api/fx/ingestor/internal/appversion/version.go @@ -0,0 +1,27 @@ +package appversion + +import ( + "github.com/tech/sendico/pkg/version" + vf "github.com/tech/sendico/pkg/version/factory" +) + +// Build information. Populated at build-time. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +func Create() version.Printer { + vi := version.Info{ + Program: "MeetX Connectica FX Ingestor Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&vi) +} diff --git a/api/fx/ingestor/internal/config/config.go b/api/fx/ingestor/internal/config/config.go new file mode 100644 index 0000000..d01f31c --- /dev/null +++ b/api/fx/ingestor/internal/config/config.go @@ -0,0 +1,147 @@ +package config + +import ( + "os" + "strings" + "time" + + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + mmodel "github.com/tech/sendico/fx/ingestor/internal/model" + "github.com/tech/sendico/pkg/db" + "gopkg.in/yaml.v3" +) + +const defaultPollInterval = 30 * time.Second + +type Config struct { + PollIntervalSeconds int `yaml:"poll_interval_seconds"` + Market MarketConfig `yaml:"market"` + Database *db.Config `yaml:"database"` + Metrics *MetricsConfig `yaml:"metrics"` + + pairs []Pair + pairsBySource map[mmodel.Driver][]PairConfig +} + +func Load(path string) (*Config, error) { + if path == "" { + return nil, fmerrors.New("config: path is empty") + } + + data, err := os.ReadFile(path) + if err != nil { + return nil, fmerrors.Wrap("config: failed to read file", err) + } + + cfg := &Config{} + if err := yaml.Unmarshal(data, cfg); err != nil { + return nil, fmerrors.Wrap("config: failed to parse yaml", err) + } + + if len(cfg.Market.Sources) == 0 { + return nil, fmerrors.New("config: no market sources configured") + } + sourceSet := make(map[mmodel.Driver]struct{}, len(cfg.Market.Sources)) + for idx := range cfg.Market.Sources { + src := &cfg.Market.Sources[idx] + if src.Driver.IsEmpty() { + return nil, fmerrors.New("config: market source driver is empty") + } + sourceSet[src.Driver] = struct{}{} + } + + if len(cfg.Market.Pairs) == 0 { + return nil, fmerrors.New("config: no pairs configured") + } + + normalizedPairs := make(map[string][]PairConfig, len(cfg.Market.Pairs)) + pairsBySource := make(map[mmodel.Driver][]PairConfig, len(cfg.Market.Pairs)) + var flattened []Pair + + for rawSource, pairList := range cfg.Market.Pairs { + driver := mmodel.Driver(rawSource) + if driver.IsEmpty() { + return nil, fmerrors.New("config: pair source is empty") + } + if _, ok := sourceSet[driver]; !ok { + return nil, fmerrors.New("config: pair references unknown source: " + driver.String()) + } + + processed := make([]PairConfig, len(pairList)) + for idx := range pairList { + pair := pairList[idx] + pair.Base = strings.ToUpper(strings.TrimSpace(pair.Base)) + pair.Quote = strings.ToUpper(strings.TrimSpace(pair.Quote)) + pair.Symbol = strings.TrimSpace(pair.Symbol) + if pair.Base == "" || pair.Quote == "" || pair.Symbol == "" { + return nil, fmerrors.New("config: pair entries must define base, quote, and symbol") + } + if strings.TrimSpace(pair.Provider) == "" { + pair.Provider = strings.ToLower(driver.String()) + } + processed[idx] = pair + flattened = append(flattened, Pair{ + PairConfig: pair, + Source: driver, + }) + } + pairsBySource[driver] = processed + normalizedPairs[driver.String()] = processed + } + + cfg.Market.Pairs = normalizedPairs + cfg.pairsBySource = pairsBySource + cfg.pairs = flattened + if cfg.Database == nil { + return nil, fmerrors.New("config: database configuration is required") + } + + if cfg.Metrics != nil && cfg.Metrics.Enabled { + cfg.Metrics.Address = strings.TrimSpace(cfg.Metrics.Address) + if cfg.Metrics.Address == "" { + cfg.Metrics.Address = ":9102" + } + } + + return cfg, nil +} + +func (c *Config) PollInterval() time.Duration { + if c == nil { + return defaultPollInterval + } + if c.PollIntervalSeconds <= 0 { + return defaultPollInterval + } + return time.Duration(c.PollIntervalSeconds) * time.Second +} + +func (c *Config) Pairs() []Pair { + if c == nil { + return nil + } + out := make([]Pair, len(c.pairs)) + copy(out, c.pairs) + return out +} + +func (c *Config) PairsBySource() map[mmodel.Driver][]PairConfig { + if c == nil { + return nil + } + out := make(map[mmodel.Driver][]PairConfig, len(c.pairsBySource)) + for driver, pairs := range c.pairsBySource { + cp := make([]PairConfig, len(pairs)) + copy(cp, pairs) + out[driver] = cp + } + return out +} + +func (c *Config) MetricsConfig() *MetricsConfig { + if c == nil || c.Metrics == nil { + return nil + } + cp := *c.Metrics + return &cp +} diff --git a/api/fx/ingestor/internal/config/market.go b/api/fx/ingestor/internal/config/market.go new file mode 100644 index 0000000..af53285 --- /dev/null +++ b/api/fx/ingestor/internal/config/market.go @@ -0,0 +1,24 @@ +package config + +import ( + mmodel "github.com/tech/sendico/fx/ingestor/internal/model" + pmodel "github.com/tech/sendico/pkg/model" +) + +type PairConfig struct { + Base string `yaml:"base"` + Quote string `yaml:"quote"` + Symbol string `yaml:"symbol"` + Provider string `yaml:"provider"` + Invert bool `yaml:"invert"` +} + +type Pair struct { + PairConfig `yaml:",inline"` + Source mmodel.Driver `yaml:"-"` +} + +type MarketConfig struct { + Sources []pmodel.DriverConfig[mmodel.Driver] `yaml:"sources"` + Pairs map[string][]PairConfig `yaml:"pairs"` +} diff --git a/api/fx/ingestor/internal/config/metrics.go b/api/fx/ingestor/internal/config/metrics.go new file mode 100644 index 0000000..012998a --- /dev/null +++ b/api/fx/ingestor/internal/config/metrics.go @@ -0,0 +1,6 @@ +package config + +type MetricsConfig struct { + Enabled bool `yaml:"enabled"` + Address string `yaml:"address"` +} diff --git a/api/fx/ingestor/internal/fmerrors/market.go b/api/fx/ingestor/internal/fmerrors/market.go new file mode 100644 index 0000000..a21ba63 --- /dev/null +++ b/api/fx/ingestor/internal/fmerrors/market.go @@ -0,0 +1,35 @@ +package fmerrors + +type Error struct { + message string + cause error +} + +func (e *Error) Error() string { + if e == nil { + return "" + } + if e.cause == nil { + return e.message + } + return e.message + ": " + e.cause.Error() +} + +func (e *Error) Unwrap() error { + if e == nil { + return nil + } + return e.cause +} + +func New(message string) error { + return &Error{message: message} +} + +func Wrap(message string, cause error) error { + return &Error{message: message, cause: cause} +} + +func NewDecimal(value string) error { + return &Error{message: "invalid decimal \"" + value + "\""} +} diff --git a/api/fx/ingestor/internal/ingestor/metrics.go b/api/fx/ingestor/internal/ingestor/metrics.go new file mode 100644 index 0000000..315d844 --- /dev/null +++ b/api/fx/ingestor/internal/ingestor/metrics.go @@ -0,0 +1,84 @@ +package ingestor + +import ( + "sync" + "time" + + "github.com/tech/sendico/fx/ingestor/internal/config" + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +type serviceMetrics struct { + pollDuration *prometheus.HistogramVec + pollTotal *prometheus.CounterVec + pairDuration *prometheus.HistogramVec + pairTotal *prometheus.CounterVec + pairLastUpdate *prometheus.GaugeVec +} + +var ( + metricsOnce sync.Once + globalMetricsRef *serviceMetrics +) + +func getServiceMetrics() *serviceMetrics { + metricsOnce.Do(func() { + reg := prometheus.DefaultRegisterer + globalMetricsRef = &serviceMetrics{ + pollDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ + Name: "fx_ingestor_poll_duration_seconds", + Help: "Duration of a polling cycle.", + Buckets: prometheus.DefBuckets, + }, []string{"result"}), + pollTotal: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ + Name: "fx_ingestor_poll_total", + Help: "Total polling cycles executed.", + }, []string{"result"}), + pairDuration: promauto.With(reg).NewHistogramVec(prometheus.HistogramOpts{ + Name: "fx_ingestor_pair_duration_seconds", + Help: "Duration of individual pair ingestion.", + Buckets: prometheus.DefBuckets, + }, []string{"source", "provider", "symbol", "result"}), + pairTotal: promauto.With(reg).NewCounterVec(prometheus.CounterOpts{ + Name: "fx_ingestor_pair_total", + Help: "Total ingestion attempts per pair.", + }, []string{"source", "provider", "symbol", "result"}), + pairLastUpdate: promauto.With(reg).NewGaugeVec(prometheus.GaugeOpts{ + Name: "fx_ingestor_pair_last_success_unix", + Help: "Unix timestamp of the last successful ingestion per pair.", + }, []string{"source", "provider", "symbol"}), + } + }) + return globalMetricsRef +} + +func (m *serviceMetrics) observePoll(duration time.Duration, err error) { + if m == nil { + return + } + result := labelForError(err) + m.pollDuration.WithLabelValues(result).Observe(duration.Seconds()) + m.pollTotal.WithLabelValues(result).Inc() +} + +func (m *serviceMetrics) observePair(pair config.Pair, duration time.Duration, err error) { + if m == nil { + return + } + result := labelForError(err) + labels := []string{pair.Source.String(), pair.Provider, pair.Symbol, result} + m.pairDuration.WithLabelValues(labels...).Observe(duration.Seconds()) + m.pairTotal.WithLabelValues(labels...).Inc() + if err == nil { + m.pairLastUpdate.WithLabelValues(pair.Source.String(), pair.Provider, pair.Symbol). + Set(float64(time.Now().Unix())) + } +} + +func labelForError(err error) string { + if err != nil { + return "error" + } + return "success" +} diff --git a/api/fx/ingestor/internal/ingestor/service.go b/api/fx/ingestor/internal/ingestor/service.go new file mode 100644 index 0000000..a3f8c54 --- /dev/null +++ b/api/fx/ingestor/internal/ingestor/service.go @@ -0,0 +1,207 @@ +package ingestor + +import ( + "context" + "math/big" + "time" + + "github.com/tech/sendico/fx/ingestor/internal/config" + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + "github.com/tech/sendico/fx/ingestor/internal/market" + mmodel "github.com/tech/sendico/fx/ingestor/internal/model" + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" +) + +type Service struct { + logger mlogger.Logger + cfg *config.Config + rates storage.RatesStore + pairs []config.Pair + connectors map[mmodel.Driver]mmodel.Connector + metrics *serviceMetrics +} + +func New(logger mlogger.Logger, cfg *config.Config, repo storage.Repository) (*Service, error) { + if logger == nil { + return nil, fmerrors.New("ingestor: nil logger") + } + if cfg == nil { + return nil, fmerrors.New("ingestor: nil config") + } + if repo == nil { + return nil, fmerrors.New("ingestor: nil repository") + } + + connectors, err := market.BuildConnectors(logger, cfg.Market.Sources) + if err != nil { + return nil, fmerrors.Wrap("build connectors", err) + } + + return &Service{ + logger: logger.Named("ingestor"), + cfg: cfg, + rates: repo.Rates(), + pairs: cfg.Pairs(), + connectors: connectors, + metrics: getServiceMetrics(), + }, nil +} + +func (s *Service) Run(ctx context.Context) error { + interval := s.cfg.PollInterval() + ticker := time.NewTicker(interval) + defer ticker.Stop() + + s.logger.Info("FX ingestion service started", zap.Duration("poll_interval", interval), zap.Int("pairs", len(s.pairs))) + + if err := s.executePoll(ctx); err != nil { + s.logger.Warn("Initial poll completed with errors", zap.Error(err)) + } + + for { + select { + case <-ctx.Done(): + s.logger.Info("Context cancelled, stopping ingestor") + return ctx.Err() + case <-ticker.C: + if err := s.executePoll(ctx); err != nil { + s.logger.Warn("Polling cycle completed with errors", zap.Error(err)) + } + } + } +} + +func (s *Service) executePoll(ctx context.Context) error { + start := time.Now() + err := s.pollOnce(ctx) + if s.metrics != nil { + s.metrics.observePoll(time.Since(start), err) + } + return err +} + +func (s *Service) pollOnce(ctx context.Context) error { + var firstErr error + for _, pair := range s.pairs { + start := time.Now() + err := s.upsertPair(ctx, pair) + elapsed := time.Since(start) + if s.metrics != nil { + s.metrics.observePair(pair, elapsed, err) + } + if err != nil { + if firstErr == nil { + firstErr = err + } + s.logger.Warn("Failed to ingest pair", + zap.String("symbol", pair.Symbol), + zap.String("source", pair.Source.String()), + zap.Duration("elapsed", elapsed), + zap.Error(err), + ) + } + } + return firstErr +} + +func (s *Service) upsertPair(ctx context.Context, pair config.Pair) error { + connector, ok := s.connectors[pair.Source] + if !ok { + return fmerrors.Wrap("connector not configured for source "+pair.Source.String(), nil) + } + + ticker, err := connector.FetchTicker(ctx, pair.Symbol) + if err != nil { + return fmerrors.Wrap("fetch ticker", err) + } + + bid, err := parseDecimal(ticker.BidPrice) + if err != nil { + return fmerrors.Wrap("parse bid price", err) + } + ask, err := parseDecimal(ticker.AskPrice) + if err != nil { + return fmerrors.Wrap("parse ask price", err) + } + + if pair.Invert { + bid, ask = invertPrices(bid, ask) + } + + if ask.Cmp(bid) < 0 { + // Ensure bid <= ask to keep downstream logic predictable. + bid, ask = ask, bid + } + + mid := new(big.Rat).Add(bid, ask) + mid.Quo(mid, big.NewRat(2, 1)) + + spread := big.NewRat(0, 1) + if mid.Sign() != 0 { + spread.Sub(ask, bid) + if spread.Sign() < 0 { + spread.Neg(spread) + } + spread.Quo(spread, mid) + spread.Mul(spread, big.NewRat(10000, 1)) // basis points + } + + now := time.Now().UTC() + asOf := now + snapshot := &model.RateSnapshot{ + RateRef: market.BuildRateReference(pair.Provider, pair.Symbol, now), + Pair: model.CurrencyPair{Base: pair.Base, Quote: pair.Quote}, + Provider: pair.Provider, + Mid: formatDecimal(mid), + Bid: formatDecimal(bid), + Ask: formatDecimal(ask), + SpreadBps: formatDecimal(spread), + AsOfUnixMs: now.UnixMilli(), + AsOf: &asOf, + Source: ticker.Provider, + ProviderRef: ticker.Symbol, + } + + if err := s.rates.UpsertSnapshot(ctx, snapshot); err != nil { + return fmerrors.Wrap("upsert snapshot", err) + } + + s.logger.Debug("Snapshot ingested", + zap.String("pair", pair.Base+"/"+pair.Quote), + zap.String("provider", pair.Provider), + zap.String("bid", snapshot.Bid), + zap.String("ask", snapshot.Ask), + zap.String("mid", snapshot.Mid), + ) + + return nil +} + +func parseDecimal(value string) (*big.Rat, error) { + r := new(big.Rat) + if _, ok := r.SetString(value); !ok { + return nil, fmerrors.NewDecimal(value) + } + return r, nil +} + +func invertPrices(bid, ask *big.Rat) (*big.Rat, *big.Rat) { + if bid.Sign() == 0 || ask.Sign() == 0 { + return bid, ask + } + one := big.NewRat(1, 1) + invBid := new(big.Rat).Quo(one, ask) // invert ask to get bid + invAsk := new(big.Rat).Quo(one, bid) // invert bid to get ask + return invBid, invAsk +} + +func formatDecimal(r *big.Rat) string { + if r == nil { + return "0" + } + // Format with 8 decimal places, trimming trailing zeros. + return r.FloatString(8) +} diff --git a/api/fx/ingestor/internal/ingestor/service_test.go b/api/fx/ingestor/internal/ingestor/service_test.go new file mode 100644 index 0000000..63fc931 --- /dev/null +++ b/api/fx/ingestor/internal/ingestor/service_test.go @@ -0,0 +1,237 @@ +package ingestor + +import ( + "context" + "errors" + "testing" + + "github.com/google/go-cmp/cmp" + "github.com/tech/sendico/fx/ingestor/internal/config" + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + mmarket "github.com/tech/sendico/fx/ingestor/internal/model" + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "go.uber.org/zap" +) + +func TestParseDecimal(t *testing.T) { + got, err := parseDecimal("123.456") + if err != nil { + t.Fatalf("parseDecimal returned error: %v", err) + } + if got.String() != "15432/125" { // 123.456 expressed as a rational + t.Fatalf("unexpected rational value: %s", got.String()) + } + + if _, err := parseDecimal("not-a-number"); err == nil { + t.Fatalf("parseDecimal should fail on invalid decimal string") + } +} + +func TestInvertPrices(t *testing.T) { + bid, err := parseDecimal("2") + if err != nil { + t.Fatalf("parseDecimal: %v", err) + } + ask, err := parseDecimal("4") + if err != nil { + t.Fatalf("parseDecimal: %v", err) + } + + invBid, invAsk := invertPrices(bid, ask) + if diff := cmp.Diff("0.5", invAsk.FloatString(1)); diff != "" { + t.Fatalf("unexpected inverted ask (-want +got):\n%s", diff) + } + if diff := cmp.Diff("0.25", invBid.FloatString(2)); diff != "" { + t.Fatalf("unexpected inverted bid (-want +got):\n%s", diff) + } +} + +func TestServiceUpsertPairStoresSnapshot(t *testing.T) { + store := &ratesStoreStub{} + svc := testService(store, map[mmarket.Driver]mmarket.Connector{ + mmarket.DriverBinance: &connectorStub{ + id: mmarket.DriverBinance, + ticker: &mmarket.Ticker{ + Symbol: "EURUSDT", + BidPrice: "1.0000", + AskPrice: "1.2000", + Provider: "binance", + }, + }, + }) + + pair := config.Pair{ + PairConfig: config.PairConfig{ + Base: "USDT", + Quote: "EUR", + Symbol: "EURUSDT", + Provider: "binance", + }, + Source: mmarket.DriverBinance, + } + + if err := svc.upsertPair(context.Background(), pair); err != nil { + t.Fatalf("upsertPair returned error: %v", err) + } + if len(store.snapshots) != 1 { + t.Fatalf("expected 1 snapshot stored, got %d", len(store.snapshots)) + } + snap := store.snapshots[0] + if snap.Pair.Base != "USDT" || snap.Pair.Quote != "EUR" { + t.Fatalf("unexpected currency pair stored: %+v", snap.Pair) + } + if snap.Provider != "binance" { + t.Fatalf("unexpected provider: %s", snap.Provider) + } + if snap.Bid != "1.00000000" || snap.Ask != "1.20000000" { + t.Fatalf("unexpected bid/ask: %s / %s", snap.Bid, snap.Ask) + } + if snap.Mid != "1.10000000" { + t.Fatalf("unexpected mid price: %s", snap.Mid) + } + if snap.SpreadBps != "1818.18181818" { + t.Fatalf("unexpected spread bps: %s", snap.SpreadBps) + } +} + +func TestServiceUpsertPairInvertsPrices(t *testing.T) { + store := &ratesStoreStub{} + svc := testService(store, map[mmarket.Driver]mmarket.Connector{ + mmarket.DriverCoinGecko: &connectorStub{ + id: mmarket.DriverCoinGecko, + ticker: &mmarket.Ticker{ + Symbol: "RUBUSDT", + BidPrice: "2", + AskPrice: "4", + Provider: "coingecko", + }, + }, + }) + + pair := config.Pair{ + PairConfig: config.PairConfig{ + Base: "RUB", + Quote: "USDT", + Symbol: "RUBUSDT", + Provider: "coingecko", + Invert: true, + }, + Source: mmarket.DriverCoinGecko, + } + + if err := svc.upsertPair(context.Background(), pair); err != nil { + t.Fatalf("upsertPair returned error: %v", err) + } + + snap := store.snapshots[0] + if snap.Bid != "0.25000000" || snap.Ask != "0.50000000" { + t.Fatalf("unexpected inverted bid/ask: %s / %s", snap.Bid, snap.Ask) + } +} + +func TestServicePollOnceReturnsFirstError(t *testing.T) { + errFetch := fmerrors.New("fetch failed") + connectorSuccess := &connectorStub{ + id: mmarket.DriverBinance, + ticker: &mmarket.Ticker{ + Symbol: "EURUSDT", + BidPrice: "1", + AskPrice: "1", + Provider: "binance", + }, + } + connectorFail := &connectorStub{ + id: mmarket.DriverCoinGecko, + err: errFetch, + } + + store := &ratesStoreStub{} + svc := testService(store, map[mmarket.Driver]mmarket.Connector{ + mmarket.DriverBinance: connectorSuccess, + mmarket.DriverCoinGecko: connectorFail, + }) + svc.pairs = []config.Pair{ + {PairConfig: config.PairConfig{Base: "USDT", Quote: "EUR", Symbol: "EURUSDT"}, Source: mmarket.DriverBinance}, + {PairConfig: config.PairConfig{Base: "USDT", Quote: "RUB", Symbol: "RUBUSDT"}, Source: mmarket.DriverCoinGecko}, + } + + err := svc.pollOnce(context.Background()) + if err == nil { + t.Fatalf("pollOnce expected to return error") + } + if !errors.Is(err, errFetch) { + t.Fatalf("pollOnce returned unexpected error: %v", err) + } + if connectorSuccess.calls != 1 { + t.Fatalf("expected success connector called once, got %d", connectorSuccess.calls) + } + if connectorFail.calls != 1 { + t.Fatalf("expected failing connector called once, got %d", connectorFail.calls) + } + if len(store.snapshots) != 1 { + t.Fatalf("expected snapshot stored only for successful pair, got %d", len(store.snapshots)) + } +} + +// -- test helpers ----------------------------------------------------------------- + +type ratesStoreStub struct { + snapshots []*model.RateSnapshot + err error +} + +func (r *ratesStoreStub) UpsertSnapshot(_ context.Context, snapshot *model.RateSnapshot) error { + if r.err != nil { + return r.err + } + cp := *snapshot + r.snapshots = append(r.snapshots, &cp) + return nil +} + +func (r *ratesStoreStub) LatestSnapshot(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) { + return nil, nil +} + +type repositoryStub struct { + rates storage.RatesStore +} + +func (r *repositoryStub) Ping(context.Context) error { return nil } +func (r *repositoryStub) Rates() storage.RatesStore { return r.rates } +func (r *repositoryStub) Quotes() storage.QuotesStore { return nil } +func (r *repositoryStub) Pairs() storage.PairStore { return nil } +func (r *repositoryStub) Currencies() storage.CurrencyStore { return nil } + +type connectorStub struct { + id mmarket.Driver + ticker *mmarket.Ticker + err error + calls int +} + +func (c *connectorStub) ID() mmarket.Driver { + return c.id +} + +func (c *connectorStub) FetchTicker(_ context.Context, symbol string) (*mmarket.Ticker, error) { + c.calls++ + if c.ticker != nil { + cp := *c.ticker + cp.Symbol = symbol + return &cp, c.err + } + return nil, c.err +} + +func testService(store storage.RatesStore, connectors map[mmarket.Driver]mmarket.Connector) *Service { + return &Service{ + logger: zap.NewNop(), + cfg: &config.Config{}, + rates: store, + connectors: connectors, + pairs: nil, + metrics: nil, + } +} diff --git a/api/fx/ingestor/internal/market/binance/connector.go b/api/fx/ingestor/internal/market/binance/connector.go new file mode 100644 index 0000000..f46e131 --- /dev/null +++ b/api/fx/ingestor/internal/market/binance/connector.go @@ -0,0 +1,139 @@ +package binance + +import ( + "context" + "encoding/json" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + "github.com/tech/sendico/fx/ingestor/internal/market/common" + mmodel "github.com/tech/sendico/fx/ingestor/internal/model" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +type binanceConnector struct { + id mmodel.Driver + provider string + client *http.Client + base string + logger mlogger.Logger +} + +const defaultBinanceBaseURL = "https://api.binance.com" +const ( + defaultDialTimeoutSeconds = 5 * time.Second + defaultDialKeepAliveSeconds = 30 * time.Second + defaultTLSHandshakeTimeoutSeconds = 5 * time.Second + defaultResponseHeaderTimeoutSeconds = 10 * time.Second + defaultRequestTimeoutSeconds = 10 * time.Second +) + +func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { + baseURL := defaultBinanceBaseURL + provider := strings.ToLower(mmodel.DriverBinance.String()) + dialTimeout := defaultDialTimeoutSeconds + dialKeepAlive := defaultDialKeepAliveSeconds + tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds + responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds + requestTimeout := defaultRequestTimeoutSeconds + + if settings != nil { + if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" { + baseURL = strings.TrimSpace(value) + } + if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" { + provider = strings.TrimSpace(value) + } + dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout) + dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive) + tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout) + responseHeaderTimeout = common.DurationSetting(settings, "response_header_timeout_seconds", responseHeaderTimeout) + requestTimeout = common.DurationSetting(settings, "request_timeout_seconds", requestTimeout) + } + + parsed, err := url.Parse(baseURL) + if err != nil { + return nil, fmerrors.Wrap("binance: invalid base url", err) + } + + transport := &http.Transport{ + DialContext: (&net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}).DialContext, + TLSHandshakeTimeout: tlsHandshakeTimeout, + ResponseHeaderTimeout: responseHeaderTimeout, + } + + connector := &binanceConnector{ + id: mmodel.DriverBinance, + provider: provider, + client: &http.Client{ + Timeout: requestTimeout, + Transport: transport, + }, + base: parsed.String(), + logger: logger.Named("binance"), + } + + return connector, nil +} + +func (c *binanceConnector) ID() mmodel.Driver { + return c.id +} + +func (c *binanceConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) { + if strings.TrimSpace(symbol) == "" { + return nil, fmerrors.New("binance: symbol is empty") + } + + endpoint, err := url.Parse(c.base) + if err != nil { + return nil, fmerrors.Wrap("binance: parse base url", err) + } + endpoint.Path = "/api/v3/ticker/bookTicker" + query := endpoint.Query() + query.Set("symbol", strings.ToUpper(strings.TrimSpace(symbol))) + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, fmerrors.Wrap("binance: build request", err) + } + + resp, err := c.client.Do(req) + if err != nil { + c.logger.Warn("Binance request failed", zap.String("symbol", symbol), zap.Error(err)) + return nil, fmerrors.Wrap("binance: request failed", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + c.logger.Warn("Binance returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode)) + return nil, fmerrors.New("binance: unexpected status " + strconv.Itoa(resp.StatusCode)) + } + + var payload struct { + Symbol string `json:"symbol"` + BidPrice string `json:"bidPrice"` + AskPrice string `json:"askPrice"` + } + + if err := json.NewDecoder(resp.Body).Decode(&payload); err != nil { + c.logger.Warn("Binance decode failed", zap.String("symbol", symbol), zap.Error(err)) + return nil, fmerrors.Wrap("binance: decode response", err) + } + + return &mmodel.Ticker{ + Symbol: payload.Symbol, + BidPrice: payload.BidPrice, + AskPrice: payload.AskPrice, + Provider: c.provider, + Timestamp: time.Now().UnixMilli(), + }, nil +} diff --git a/api/fx/ingestor/internal/market/coingecko/connector.go b/api/fx/ingestor/internal/market/coingecko/connector.go new file mode 100644 index 0000000..9b878f5 --- /dev/null +++ b/api/fx/ingestor/internal/market/coingecko/connector.go @@ -0,0 +1,222 @@ +package coingecko + +import ( + "context" + "encoding/json" + "net" + "net/http" + "net/url" + "strconv" + "strings" + "time" + + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + "github.com/tech/sendico/fx/ingestor/internal/market/common" + mmodel "github.com/tech/sendico/fx/ingestor/internal/model" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +type coingeckoConnector struct { + id mmodel.Driver + provider string + client *http.Client + base string + logger mlogger.Logger +} + +const defaultCoinGeckoBaseURL = "https://api.coingecko.com/api/v3" + +const ( + defaultDialTimeoutSeconds = 5 * time.Second + defaultDialKeepAliveSeconds = 30 * time.Second + defaultTLSHandshakeTimeoutSeconds = 5 * time.Second + defaultResponseHeaderTimeoutSeconds = 10 * time.Second + defaultRequestTimeoutSeconds = 10 * time.Second +) + +func NewConnector(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) { + baseURL := defaultCoinGeckoBaseURL + provider := strings.ToLower(mmodel.DriverCoinGecko.String()) + dialTimeout := defaultDialTimeoutSeconds + dialKeepAlive := defaultDialKeepAliveSeconds + tlsHandshakeTimeout := defaultTLSHandshakeTimeoutSeconds + responseHeaderTimeout := defaultResponseHeaderTimeoutSeconds + requestTimeout := defaultRequestTimeoutSeconds + + if settings != nil { + if value, ok := settings["base_url"].(string); ok && strings.TrimSpace(value) != "" { + baseURL = strings.TrimSpace(value) + } + if value, ok := settings["provider"].(string); ok && strings.TrimSpace(value) != "" { + provider = strings.TrimSpace(value) + } + dialTimeout = common.DurationSetting(settings, "dial_timeout_seconds", dialTimeout) + dialKeepAlive = common.DurationSetting(settings, "dial_keep_alive_seconds", dialKeepAlive) + tlsHandshakeTimeout = common.DurationSetting(settings, "tls_handshake_timeout_seconds", tlsHandshakeTimeout) + responseHeaderTimeout = common.DurationSetting(settings, "response_header_timeout_seconds", responseHeaderTimeout) + requestTimeout = common.DurationSetting(settings, "request_timeout_seconds", requestTimeout) + } + + parsed, err := url.Parse(baseURL) + if err != nil { + return nil, fmerrors.Wrap("coingecko: invalid base url", err) + } + + transport := &http.Transport{ + DialContext: (&net.Dialer{Timeout: dialTimeout, KeepAlive: dialKeepAlive}).DialContext, + TLSHandshakeTimeout: tlsHandshakeTimeout, + ResponseHeaderTimeout: responseHeaderTimeout, + } + + connector := &coingeckoConnector{ + id: mmodel.DriverCoinGecko, + provider: provider, + client: &http.Client{ + Timeout: requestTimeout, + Transport: transport, + }, + base: strings.TrimRight(parsed.String(), "/"), + logger: logger.Named("coingecko"), + } + + return connector, nil +} + +func (c *coingeckoConnector) ID() mmodel.Driver { + return c.id +} + +func (c *coingeckoConnector) FetchTicker(ctx context.Context, symbol string) (*mmodel.Ticker, error) { + coinID, vsCurrency, err := parseSymbol(symbol) + if err != nil { + return nil, err + } + + endpoint, err := url.Parse(c.base) + if err != nil { + return nil, fmerrors.Wrap("coingecko: parse base url", err) + } + endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/simple/price" + query := endpoint.Query() + query.Set("ids", coinID) + query.Set("vs_currencies", vsCurrency) + query.Set("include_last_updated_at", "true") + endpoint.RawQuery = query.Encode() + + req, err := http.NewRequestWithContext(ctx, http.MethodGet, endpoint.String(), nil) + if err != nil { + return nil, fmerrors.Wrap("coingecko: build request", err) + } + + resp, err := c.client.Do(req) + if err != nil { + c.logger.Warn("CoinGecko request failed", zap.String("symbol", symbol), zap.Error(err)) + return nil, fmerrors.Wrap("coingecko: request failed", err) + } + defer resp.Body.Close() + + if resp.StatusCode != http.StatusOK { + c.logger.Warn("CoinGecko returned non-OK status", zap.String("symbol", symbol), zap.Int("status", resp.StatusCode)) + return nil, fmerrors.New("coingecko: unexpected status " + strconv.Itoa(resp.StatusCode)) + } + + decoder := json.NewDecoder(resp.Body) + decoder.UseNumber() + + var payload map[string]map[string]interface{} + if err := decoder.Decode(&payload); err != nil { + c.logger.Warn("CoinGecko decode failed", zap.String("symbol", symbol), zap.Error(err)) + return nil, fmerrors.Wrap("coingecko: decode response", err) + } + + coinData, ok := payload[coinID] + if !ok { + return nil, fmerrors.New("coingecko: coin id not found in response") + } + priceValue, ok := coinData[vsCurrency] + if !ok { + return nil, fmerrors.New("coingecko: vs currency not found in response") + } + + price, ok := toFloat(priceValue) + if !ok || price <= 0 { + return nil, fmerrors.New("coingecko: invalid price value in response") + } + + priceStr := strconv.FormatFloat(price, 'f', -1, 64) + + timestamp := time.Now().UnixMilli() + if tsValue, ok := coinData["last_updated_at"]; ok { + if tsFloat, ok := toFloat(tsValue); ok && tsFloat > 0 { + tsMillis := int64(tsFloat * 1000) + if tsMillis > 0 { + timestamp = tsMillis + } + } + } + + refSymbol := coinID + "_" + vsCurrency + + return &mmodel.Ticker{ + Symbol: refSymbol, + BidPrice: priceStr, + AskPrice: priceStr, + Provider: c.provider, + Timestamp: timestamp, + }, nil +} + +func parseSymbol(symbol string) (string, string, error) { + trimmed := strings.TrimSpace(symbol) + if trimmed == "" { + return "", "", fmerrors.New("coingecko: symbol is empty") + } + + parts := strings.FieldsFunc(strings.ToLower(trimmed), func(r rune) bool { + switch r { + case ':', '/', '-', '_': + return true + } + return false + }) + + if len(parts) != 2 { + return "", "", fmerrors.New("coingecko: symbol must be /") + } + + coinID := strings.TrimSpace(parts[0]) + vsCurrency := strings.TrimSpace(parts[1]) + if coinID == "" || vsCurrency == "" { + return "", "", fmerrors.New("coingecko: symbol contains empty segments") + } + + return coinID, vsCurrency, nil +} + +func toFloat(value interface{}) (float64, bool) { + switch v := value.(type) { + case json.Number: + f, err := v.Float64() + if err != nil { + return 0, false + } + return f, true + case float64: + return v, true + case float32: + return float64(v), true + case int: + return float64(v), true + case int64: + return float64(v), true + case uint64: + return float64(v), true + case string: + if parsed, err := strconv.ParseFloat(v, 64); err == nil { + return parsed, true + } + } + return 0, false +} diff --git a/api/fx/ingestor/internal/market/common/settings.go b/api/fx/ingestor/internal/market/common/settings.go new file mode 100644 index 0000000..e58c450 --- /dev/null +++ b/api/fx/ingestor/internal/market/common/settings.go @@ -0,0 +1,46 @@ +package common + +import ( + "strconv" + "time" + + "github.com/tech/sendico/pkg/model" +) + +// DurationSetting reads a positive duration override from settings or returns def when the value is missing or invalid. +func DurationSetting(settings model.SettingsT, key string, def time.Duration) time.Duration { + if settings == nil { + return def + } + value, ok := settings[key] + if !ok { + return def + } + + switch v := value.(type) { + case time.Duration: + if v > 0 { + return v + } + case int: + if v > 0 { + return time.Duration(v) * time.Second + } + case int64: + if v > 0 { + return time.Duration(v) * time.Second + } + case float64: + if v > 0 { + return time.Duration(v * float64(time.Second)) + } + case string: + if parsed, err := time.ParseDuration(v); err == nil && parsed > 0 { + return parsed + } + if seconds, err := strconv.ParseFloat(v, 64); err == nil && seconds > 0 { + return time.Duration(seconds * float64(time.Second)) + } + } + return def +} diff --git a/api/fx/ingestor/internal/market/factory.go b/api/fx/ingestor/internal/market/factory.go new file mode 100644 index 0000000..e2d0a5f --- /dev/null +++ b/api/fx/ingestor/internal/market/factory.go @@ -0,0 +1,55 @@ +package market + +import ( + "strconv" + "strings" + "time" + + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + "github.com/tech/sendico/fx/ingestor/internal/market/binance" + "github.com/tech/sendico/fx/ingestor/internal/market/coingecko" + mmodel "github.com/tech/sendico/fx/ingestor/internal/model" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" +) + +type ConnectorFactory func(logger mlogger.Logger, settings model.SettingsT) (mmodel.Connector, error) + +func BuildConnectors(logger mlogger.Logger, configs []model.DriverConfig[mmodel.Driver]) (map[mmodel.Driver]mmodel.Connector, error) { + connectors := make(map[mmodel.Driver]mmodel.Connector, len(configs)) + + for _, cfg := range configs { + driver := mmodel.NormalizeDriver(cfg.Driver) + if driver.IsEmpty() { + return nil, fmerrors.New("market: connector driver is empty") + } + + var ( + conn mmodel.Connector + err error + ) + + switch driver { + case mmodel.DriverBinance: + conn, err = binance.NewConnector(logger, cfg.Settings) + case mmodel.DriverCoinGecko: + conn, err = coingecko.NewConnector(logger, cfg.Settings) + default: + err = fmerrors.New("market: unsupported driver " + driver.String()) + } + + if err != nil { + return nil, fmerrors.Wrap("market: build connector "+driver.String(), err) + } + connectors[driver] = conn + } + + return connectors, nil +} + +func BuildRateReference(provider, symbol string, now time.Time) string { + if strings.TrimSpace(provider) == "" { + provider = "unknown" + } + return provider + ":" + symbol + ":" + strconv.FormatInt(now.UnixMilli(), 10) +} diff --git a/api/fx/ingestor/internal/metrics/server.go b/api/fx/ingestor/internal/metrics/server.go new file mode 100644 index 0000000..b7405cf --- /dev/null +++ b/api/fx/ingestor/internal/metrics/server.go @@ -0,0 +1,134 @@ +package metrics + +import ( + "context" + "errors" + "net/http" + "strings" + "time" + + "github.com/go-chi/chi/v5" + "github.com/tech/sendico/fx/ingestor/internal/config" + "github.com/tech/sendico/fx/ingestor/internal/fmerrors" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/api/routers/health" + "github.com/tech/sendico/pkg/mlogger" + "github.com/prometheus/client_golang/prometheus/promhttp" + "go.uber.org/zap" +) + +const ( + defaultAddress = ":9102" + readHeaderTimeout = 5 * time.Second + defaultShutdownWindow = 5 * time.Second +) + +type Server interface { + SetStatus(health.ServiceStatus) + Close(context.Context) +} + +func NewServer(logger mlogger.Logger, cfg *config.MetricsConfig) (Server, error) { + if logger == nil { + return nil, fmerrors.New("metrics: logger is nil") + } + if cfg == nil || !cfg.Enabled { + logger.Debug("Metrics disabled; using noop server") + return noopServer{}, nil + } + + address := strings.TrimSpace(cfg.Address) + if address == "" { + address = defaultAddress + } + + metricsLogger := logger.Named("metrics") + router := chi.NewRouter() + router.Handle("/metrics", promhttp.Handler()) + + var healthRouter routers.Health + if hr, err := routers.NewHealthRouter(metricsLogger, router, ""); err != nil { + metricsLogger.Warn("Failed to initialise health router", zap.Error(err)) + } else { + hr.SetStatus(health.SSStarting) + healthRouter = hr + } + + httpServer := &http.Server{ + Addr: address, + Handler: router, + ReadHeaderTimeout: readHeaderTimeout, + } + + ms := &httpServerWrapper{ + logger: metricsLogger, + server: httpServer, + health: healthRouter, + timeout: defaultShutdownWindow, + } + + go func() { + metricsLogger.Info("Prometheus endpoint listening", zap.String("address", address)) + if err := httpServer.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { + metricsLogger.Error("Prometheus endpoint stopped unexpectedly", zap.Error(err)) + if healthRouter != nil { + healthRouter.SetStatus(health.SSTerminating) + } + } + }() + + return ms, nil +} + +type httpServerWrapper struct { + logger mlogger.Logger + server *http.Server + health routers.Health + timeout time.Duration +} + +func (s *httpServerWrapper) SetStatus(status health.ServiceStatus) { + if s == nil || s.health == nil { + return + } + s.logger.Debug("Updating metrics health status", zap.String("status", string(status))) + s.health.SetStatus(status) +} + +func (s *httpServerWrapper) Close(ctx context.Context) { + if s == nil { + return + } + + if s.health != nil { + s.health.SetStatus(health.SSTerminating) + s.health.Finish() + s.health = nil + } + + if s.server == nil { + return + } + + shutdownCtx := ctx + if shutdownCtx == nil { + shutdownCtx = context.Background() + } + if s.timeout > 0 { + var cancel context.CancelFunc + shutdownCtx, cancel = context.WithTimeout(shutdownCtx, s.timeout) + defer cancel() + } + + if err := s.server.Shutdown(shutdownCtx); err != nil && !errors.Is(err, http.ErrServerClosed) { + s.logger.Warn("Failed to stop metrics server", zap.Error(err)) + } else { + s.logger.Info("Metrics server stopped") + } +} + +type noopServer struct{} + +func (noopServer) SetStatus(health.ServiceStatus) {} + +func (noopServer) Close(context.Context) {} diff --git a/api/fx/ingestor/internal/model/connector.go b/api/fx/ingestor/internal/model/connector.go new file mode 100644 index 0000000..7dc4f9c --- /dev/null +++ b/api/fx/ingestor/internal/model/connector.go @@ -0,0 +1,30 @@ +package model + +import ( + "context" + "strings" +) + +type Driver string + +const ( + DriverBinance Driver = "BINANCE" + DriverCoinGecko Driver = "COINGECKO" +) + +func (d Driver) String() string { + return string(d) +} + +func (d Driver) IsEmpty() bool { + return strings.TrimSpace(string(d)) == "" +} + +func NormalizeDriver(d Driver) Driver { + return Driver(strings.ToUpper(strings.TrimSpace(string(d)))) +} + +type Connector interface { + ID() Driver + FetchTicker(ctx context.Context, symbol string) (*Ticker, error) +} diff --git a/api/fx/ingestor/internal/model/ticker.go b/api/fx/ingestor/internal/model/ticker.go new file mode 100644 index 0000000..ae2431a --- /dev/null +++ b/api/fx/ingestor/internal/model/ticker.go @@ -0,0 +1,9 @@ +package model + +type Ticker struct { + Symbol string + BidPrice string + AskPrice string + Provider string + Timestamp int64 +} diff --git a/api/fx/ingestor/internal/signalctx/signalctx.go b/api/fx/ingestor/internal/signalctx/signalctx.go new file mode 100644 index 0000000..6c471ca --- /dev/null +++ b/api/fx/ingestor/internal/signalctx/signalctx.go @@ -0,0 +1,14 @@ +package signalctx + +import ( + "context" + "os" + "os/signal" +) + +func WithSignals(parent context.Context, sig ...os.Signal) (context.Context, context.CancelFunc) { + if parent == nil { + parent = context.Background() + } + return signal.NotifyContext(parent, sig...) +} diff --git a/api/fx/ingestor/main.go b/api/fx/ingestor/main.go new file mode 100644 index 0000000..f2ad5dd --- /dev/null +++ b/api/fx/ingestor/main.go @@ -0,0 +1,55 @@ +package main + +import ( + "context" + "errors" + "flag" + "fmt" + "os" + "syscall" + + "github.com/tech/sendico/fx/ingestor/internal/app" + "github.com/tech/sendico/fx/ingestor/internal/appversion" + "github.com/tech/sendico/fx/ingestor/internal/signalctx" + lf "github.com/tech/sendico/pkg/mlogger/factory" + "go.uber.org/zap" +) + +var ( + configFile = flag.String("config.file", app.DefaultConfigPath, "Path to the configuration file.") + debugFlag = flag.Bool("debug", false, "Enable debug logging.") + versionFlag = flag.Bool("version", false, "Show version information.") +) + +func main() { + flag.Parse() + + logger := lf.NewLogger(*debugFlag).Named("fx_ingestor") + defer logger.Sync() + + av := appversion.Create() + if *versionFlag { + fmt.Fprintln(os.Stdout, av.Print()) + return + } + + logger.Info(fmt.Sprintf("Starting %s", av.Program()), zap.String("version", av.Info())) + + ctx, cancel := signalctx.WithSignals(context.Background(), os.Interrupt, syscall.SIGTERM) + defer cancel() + + application, err := app.New(logger, *configFile) + if err != nil { + logger.Fatal("Failed to initialise application", zap.Error(err)) + } + + if err := application.Run(ctx); err != nil { + if errors.Is(err, context.Canceled) { + logger.Info("FX ingestor stopped") + return + } + logger.Fatal("Ingestor terminated with error", zap.Error(err)) + } + + logger.Info("FX ingestor stopped") +} diff --git a/api/fx/oracle/.air.toml b/api/fx/oracle/.air.toml new file mode 100644 index 0000000..a6e2c0d --- /dev/null +++ b/api/fx/oracle/.air.toml @@ -0,0 +1,32 @@ +# Config file for Air in TOML format + +root = "./../.." +tmp_dir = "tmp" + +[build] +cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/fx/oracle/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/fx/oracle/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/fx/oracle/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/fx/oracle/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/fx/oracle/internal/appversion.BuildDate=$(date)'\"" +bin = "./app" +full_bin = "./app --debug --config.file=config.yml" +include_ext = ["go", "yaml", "yml"] +exclude_dir = ["fx/oracle/tmp", "pkg/.git", "fx/oracle/env"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true +follow_symlink = true +log = "air.log" +delay = 0 +stop_on_error = true +send_interrupt = true +kill_delay = 500 +args_bin = [] + +[log] +time = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true diff --git a/api/fx/oracle/.gitignore b/api/fx/oracle/.gitignore new file mode 100644 index 0000000..dc67a7e --- /dev/null +++ b/api/fx/oracle/.gitignore @@ -0,0 +1,3 @@ +internal/generated +.gocache +app \ No newline at end of file diff --git a/api/fx/oracle/client/client.go b/api/fx/oracle/client/client.go new file mode 100644 index 0000000..82ab2d2 --- /dev/null +++ b/api/fx/oracle/client/client.go @@ -0,0 +1,252 @@ +package client + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "strings" + "time" + + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Client exposes typed helpers around the oracle gRPC API. +type Client interface { + LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) + GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) + Close() error +} + +// RequestMeta carries optional multi-tenant context for oracle calls. +type RequestMeta struct { + TenantRef string + OrganizationRef string + Trace *tracev1.TraceContext +} + +type LatestRateParams struct { + Meta RequestMeta + Pair *fxv1.CurrencyPair + Provider string +} + +type RateSnapshot struct { + Pair *fxv1.CurrencyPair + Mid string + Bid string + Ask string + SpreadBps string + Provider string + RateRef string + AsOf time.Time +} + +type GetQuoteParams struct { + Meta RequestMeta + Pair *fxv1.CurrencyPair + Side fxv1.Side + BaseAmount *moneyv1.Money + QuoteAmount *moneyv1.Money + Firm bool + TTL time.Duration + PreferredProvider string + MaxAge time.Duration +} + +type Quote struct { + QuoteRef string + Pair *fxv1.CurrencyPair + Side fxv1.Side + Price string + BaseAmount *moneyv1.Money + QuoteAmount *moneyv1.Money + ExpiresAt time.Time + Provider string + RateRef string + Firm bool +} + +type grpcOracleClient interface { + GetQuote(ctx context.Context, in *oraclev1.GetQuoteRequest, opts ...grpc.CallOption) (*oraclev1.GetQuoteResponse, error) + LatestRate(ctx context.Context, in *oraclev1.LatestRateRequest, opts ...grpc.CallOption) (*oraclev1.LatestRateResponse, error) +} + +type oracleClient struct { + cfg Config + conn *grpc.ClientConn + client grpcOracleClient +} + +// New dials the oracle endpoint and returns a ready client. +func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { + cfg.setDefaults() + if strings.TrimSpace(cfg.Address) == "" { + return nil, errors.New("oracle: address is required") + } + + dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) + defer cancel() + + dialOpts := make([]grpc.DialOption, 0, len(opts)+1) + dialOpts = append(dialOpts, opts...) + + if cfg.Insecure { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + } + + conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) + if err != nil { + return nil, fmt.Errorf("oracle: dial %s: %w", cfg.Address, err) + } + + return &oracleClient{ + cfg: cfg, + conn: conn, + client: oraclev1.NewOracleClient(conn), + }, nil +} + +// NewWithClient injects a pre-built oracle client (useful for tests). +func NewWithClient(cfg Config, oc grpcOracleClient) Client { + cfg.setDefaults() + return &oracleClient{ + cfg: cfg, + client: oc, + } +} + +func (c *oracleClient) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +func (c *oracleClient) LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) { + if req.Pair == nil { + return nil, errors.New("oracle: pair is required") + } + + callCtx, cancel := c.callContext(ctx) + defer cancel() + + resp, err := c.client.LatestRate(callCtx, &oraclev1.LatestRateRequest{ + Meta: toProtoMeta(req.Meta), + Pair: req.Pair, + Provider: req.Provider, + }) + if err != nil { + return nil, fmt.Errorf("oracle: latest rate: %w", err) + } + if resp.GetRate() == nil { + return nil, errors.New("oracle: latest rate: empty payload") + } + return fromProtoRate(resp.GetRate()), nil +} + +func (c *oracleClient) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) { + if req.Pair == nil { + return nil, errors.New("oracle: pair is required") + } + if req.Side == fxv1.Side_SIDE_UNSPECIFIED { + return nil, errors.New("oracle: side is required") + } + + baseSupplied := req.BaseAmount != nil + quoteSupplied := req.QuoteAmount != nil + if baseSupplied == quoteSupplied { + return nil, errors.New("oracle: exactly one of base_amount or quote_amount must be set") + } + + callCtx, cancel := c.callContext(ctx) + defer cancel() + + protoReq := &oraclev1.GetQuoteRequest{ + Meta: toProtoMeta(req.Meta), + Pair: req.Pair, + Side: req.Side, + Firm: req.Firm, + PreferredProvider: req.PreferredProvider, + } + if req.TTL > 0 { + protoReq.TtlMs = req.TTL.Milliseconds() + } + if req.MaxAge > 0 { + protoReq.MaxAgeMs = int32(req.MaxAge.Milliseconds()) + } + if baseSupplied { + protoReq.AmountInput = &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: req.BaseAmount} + } else { + protoReq.AmountInput = &oraclev1.GetQuoteRequest_QuoteAmount{QuoteAmount: req.QuoteAmount} + } + + resp, err := c.client.GetQuote(callCtx, protoReq) + if err != nil { + return nil, fmt.Errorf("oracle: get quote: %w", err) + } + if resp.GetQuote() == nil { + return nil, errors.New("oracle: get quote: empty payload") + } + return fromProtoQuote(resp.GetQuote()), nil +} + +func (c *oracleClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { + if _, ok := ctx.Deadline(); ok { + return context.WithCancel(ctx) + } + return context.WithTimeout(ctx, c.cfg.CallTimeout) +} + +func toProtoMeta(meta RequestMeta) *oraclev1.RequestMeta { + if meta.TenantRef == "" && meta.OrganizationRef == "" && meta.Trace == nil { + return nil + } + return &oraclev1.RequestMeta{ + TenantRef: meta.TenantRef, + OrganizationRef: meta.OrganizationRef, + Trace: meta.Trace, + } +} + +func fromProtoRate(rate *oraclev1.RateSnapshot) *RateSnapshot { + if rate == nil { + return nil + } + return &RateSnapshot{ + Pair: rate.Pair, + Mid: rate.GetMid().GetValue(), + Bid: rate.GetBid().GetValue(), + Ask: rate.GetAsk().GetValue(), + SpreadBps: rate.GetSpreadBps().GetValue(), + Provider: rate.GetProvider(), + RateRef: rate.GetRateRef(), + AsOf: time.UnixMilli(rate.GetAsofUnixMs()), + } +} + +func fromProtoQuote(quote *oraclev1.Quote) *Quote { + if quote == nil { + return nil + } + return &Quote{ + QuoteRef: quote.GetQuoteRef(), + Pair: quote.Pair, + Side: quote.GetSide(), + Price: quote.GetPrice().GetValue(), + BaseAmount: quote.BaseAmount, + QuoteAmount: quote.QuoteAmount, + ExpiresAt: time.UnixMilli(quote.GetExpiresAtUnixMs()), + Provider: quote.GetProvider(), + RateRef: quote.GetRateRef(), + Firm: quote.GetFirm(), + } +} diff --git a/api/fx/oracle/client/client_test.go b/api/fx/oracle/client/client_test.go new file mode 100644 index 0000000..3a2aca1 --- /dev/null +++ b/api/fx/oracle/client/client_test.go @@ -0,0 +1,116 @@ +package client + +import ( + "context" + "testing" + "time" + + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + "google.golang.org/grpc" +) + +type stubOracle struct { + latestResp *oraclev1.LatestRateResponse + latestErr error + + quoteResp *oraclev1.GetQuoteResponse + quoteErr error + + lastLatest *oraclev1.LatestRateRequest + lastQuote *oraclev1.GetQuoteRequest +} + +func (s *stubOracle) LatestRate(ctx context.Context, in *oraclev1.LatestRateRequest, _ ...grpc.CallOption) (*oraclev1.LatestRateResponse, error) { + s.lastLatest = in + return s.latestResp, s.latestErr +} + +func (s *stubOracle) GetQuote(ctx context.Context, in *oraclev1.GetQuoteRequest, _ ...grpc.CallOption) (*oraclev1.GetQuoteResponse, error) { + s.lastQuote = in + return s.quoteResp, s.quoteErr +} + +func TestLatestRate(t *testing.T) { + expectedTime := time.Date(2024, 1, 1, 15, 0, 0, 0, time.UTC) + stub := &stubOracle{ + latestResp: &oraclev1.LatestRateResponse{ + Rate: &oraclev1.RateSnapshot{ + Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}, + Mid: &moneyv1.Decimal{Value: "1.1000"}, + Bid: &moneyv1.Decimal{Value: "1.0995"}, + Ask: &moneyv1.Decimal{Value: "1.1005"}, + SpreadBps: &moneyv1.Decimal{Value: "5"}, + Provider: "ECB", + RateRef: "ECB-20240101", + AsofUnixMs: expectedTime.UnixMilli(), + }, + }, + } + + client := NewWithClient(Config{}, stub) + resp, err := client.LatestRate(context.Background(), LatestRateParams{ + Meta: RequestMeta{ + TenantRef: "tenant", + OrganizationRef: "org", + }, + Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}, + Provider: "ECB", + }) + if err != nil { + t.Fatalf("LatestRate returned error: %v", err) + } + + if stub.lastLatest.GetProvider() != "ECB" { + t.Fatalf("expected provider to propagate, got %s", stub.lastLatest.GetProvider()) + } + if resp.Provider != "ECB" || resp.RateRef != "ECB-20240101" { + t.Fatalf("unexpected response: %+v", resp) + } + if !resp.AsOf.Equal(expectedTime) { + t.Fatalf("expected as-of %s, got %s", expectedTime, resp.AsOf) + } +} + +func TestGetQuote(t *testing.T) { + expiresAt := time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC) + stub := &stubOracle{ + quoteResp: &oraclev1.GetQuoteResponse{ + Quote: &oraclev1.Quote{ + QuoteRef: "quote-123", + Pair: &fxv1.CurrencyPair{Base: "GBP", Quote: "USD"}, + Side: fxv1.Side_BUY_BASE_SELL_QUOTE, + Price: &moneyv1.Decimal{Value: "1.2500"}, + BaseAmount: &moneyv1.Money{Amount: "100.00", Currency: "GBP"}, + QuoteAmount: &moneyv1.Money{Amount: "125.00", Currency: "USD"}, + ExpiresAtUnixMs: expiresAt.UnixMilli(), + Provider: "Test", + RateRef: "test-ref", + Firm: true, + }, + }, + } + + client := NewWithClient(Config{}, stub) + resp, err := client.GetQuote(context.Background(), GetQuoteParams{ + Pair: &fxv1.CurrencyPair{Base: "GBP", Quote: "USD"}, + Side: fxv1.Side_BUY_BASE_SELL_QUOTE, + BaseAmount: &moneyv1.Money{Amount: "100.00", Currency: "GBP"}, + Firm: true, + TTL: 2 * time.Second, + }) + if err != nil { + t.Fatalf("GetQuote returned error: %v", err) + } + + if stub.lastQuote.GetFirm() != true { + t.Fatalf("expected firm flag to propagate") + } + if stub.lastQuote.GetTtlMs() == 0 { + t.Fatalf("expected ttl to be populated") + } + if resp.QuoteRef != "quote-123" || resp.Price != "1.2500" || !resp.ExpiresAt.Equal(expiresAt) { + t.Fatalf("unexpected quote response: %+v", resp) + } +} diff --git a/api/fx/oracle/client/config.go b/api/fx/oracle/client/config.go new file mode 100644 index 0000000..2e89b00 --- /dev/null +++ b/api/fx/oracle/client/config.go @@ -0,0 +1,20 @@ +package client + +import "time" + +// Config captures connection settings for the FX oracle gRPC service. +type Config struct { + Address string + DialTimeout time.Duration + CallTimeout time.Duration + Insecure bool +} + +func (c *Config) setDefaults() { + if c.DialTimeout <= 0 { + c.DialTimeout = 5 * time.Second + } + if c.CallTimeout <= 0 { + c.CallTimeout = 3 * time.Second + } +} diff --git a/api/fx/oracle/client/fake.go b/api/fx/oracle/client/fake.go new file mode 100644 index 0000000..3db8dc8 --- /dev/null +++ b/api/fx/oracle/client/fake.go @@ -0,0 +1,60 @@ +package client + +import ( + "context" + + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" +) + +// Fake implements Client for tests. +type Fake struct { + LatestRateFn func(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) + GetQuoteFn func(ctx context.Context, req GetQuoteParams) (*Quote, error) + CloseFn func() error +} + +func (f *Fake) LatestRate(ctx context.Context, req LatestRateParams) (*RateSnapshot, error) { + if f.LatestRateFn != nil { + return f.LatestRateFn(ctx, req) + } + return &RateSnapshot{ + Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}, + Mid: "1.1000", + Bid: "1.0995", + Ask: "1.1005", + SpreadBps: "5", + Provider: "fake", + RateRef: "fake", + }, nil +} + +func (f *Fake) GetQuote(ctx context.Context, req GetQuoteParams) (*Quote, error) { + if f.GetQuoteFn != nil { + return f.GetQuoteFn(ctx, req) + } + return &Quote{ + QuoteRef: "fake-quote", + Pair: req.Pair, + Side: req.Side, + Price: "1.1000", + BaseAmount: &moneyv1.Money{ + Amount: "100.00", + Currency: req.Pair.GetBase(), + }, + QuoteAmount: &moneyv1.Money{ + Amount: "110.00", + Currency: req.Pair.GetQuote(), + }, + Provider: "fake", + RateRef: "fake", + Firm: req.Firm, + }, nil +} + +func (f *Fake) Close() error { + if f.CloseFn != nil { + return f.CloseFn() + } + return nil +} diff --git a/api/fx/oracle/config.yml b/api/fx/oracle/config.yml new file mode 100644 index 0000000..0a01593 --- /dev/null +++ b/api/fx/oracle/config.yml @@ -0,0 +1,34 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50051" + enable_reflection: true + enable_health: true + +metrics: + address: ":9400" + +database: + driver: mongodb + settings: + host_env: FX_MONGO_HOST + port_env: FX_MONGO_PORT + database_env: FX_MONGO_DATABASE + user_env: FX_MONGO_USER + password_env: FX_MONGO_PASSWORD + auth_source_env: FX_MONGO_AUTH_SOURCE + replica_set_env: FX_MONGO_REPLICA_SET + +messaging: + driver: NATS + settings: + url_env: NATS_URL + host_env: NATS_HOST + port_env: NATS_PORT + username_env: NATS_USER + password_env: NATS_PASSWORD + broker_name: FX Oracle + max_reconnects: 10 + reconnect_wait: 5 diff --git a/api/fx/oracle/env/.gitignore b/api/fx/oracle/env/.gitignore new file mode 100644 index 0000000..f2a8cbe --- /dev/null +++ b/api/fx/oracle/env/.gitignore @@ -0,0 +1 @@ +.env.api diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod new file mode 100644 index 0000000..c605992 --- /dev/null +++ b/api/fx/oracle/go.mod @@ -0,0 +1,54 @@ +module github.com/tech/sendico/fx/oracle + +go 1.25.3 + +replace github.com/tech/sendico/pkg => ../../pkg + +replace github.com/tech/sendico/fx/storage => ../storage + +require ( + github.com/google/uuid v1.6.0 + github.com/prometheus/client_golang v1.23.2 + github.com/tech/sendico/fx/storage v0.0.0 + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver v1.17.6 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect +) diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum new file mode 100644 index 0000000..1558ea2 --- /dev/null +++ b/api/fx/oracle/go.sum @@ -0,0 +1,225 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= +github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +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/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +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/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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/fx/oracle/internal/appversion/version.go b/api/fx/oracle/internal/appversion/version.go new file mode 100644 index 0000000..cbadc55 --- /dev/null +++ b/api/fx/oracle/internal/appversion/version.go @@ -0,0 +1,27 @@ +package appversion + +import ( + "github.com/tech/sendico/pkg/version" + vf "github.com/tech/sendico/pkg/version/factory" +) + +// Build information. Populated at build-time. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +func Create() version.Printer { + vi := version.Info{ + Program: "MeetX Connectica FX Oracle Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&vi) +} diff --git a/api/fx/oracle/internal/server/internal/serverimp.go b/api/fx/oracle/internal/server/internal/serverimp.go new file mode 100644 index 0000000..5946005 --- /dev/null +++ b/api/fx/oracle/internal/server/internal/serverimp.go @@ -0,0 +1,101 @@ +package serverimp + +import ( + "context" + "os" + "time" + + "github.com/tech/sendico/fx/oracle/internal/service/oracle" + "github.com/tech/sendico/fx/storage" + mongostorage "github.com/tech/sendico/fx/storage/mongo" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/db" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type Imp struct { + logger mlogger.Logger + file string + debug bool + + config *grpcapp.Config + app *grpcapp.App[storage.Repository] +} + +func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { + return &Imp{ + logger: logger.Named("server"), + file: file, + debug: debug, + }, nil +} + +func (i *Imp) Shutdown() { + if i.app == nil { + return + } + timeout := 15 * time.Second + if i.config != nil && i.config.Runtime != nil { + timeout = i.config.Runtime.ShutdownTimeout() + } + ctx, cancel := context.WithTimeout(context.Background(), timeout) + i.app.Shutdown(ctx) + cancel() +} + +func (i *Imp) Start() error { + cfg, err := i.loadConfig() + if err != nil { + return err + } + i.config = cfg + + repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { + return mongostorage.New(logger, conn) + } + + serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { + return oracle.NewService(logger, repo, producer), nil + } + + app, err := grpcapp.NewApp(i.logger, "fx_oracle", cfg, i.debug, repoFactory, serviceFactory) + if err != nil { + return err + } + i.app = app + + return i.app.Start() +} + +func (i *Imp) loadConfig() (*grpcapp.Config, error) { + data, err := os.ReadFile(i.file) + if err != nil { + i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) + return nil, err + } + + cfg := &grpcapp.Config{} + if err := yaml.Unmarshal(data, cfg); err != nil { + i.logger.Error("Failed to parse configuration", zap.Error(err)) + return nil, err + } + + if cfg.Runtime == nil { + cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} + } + + if cfg.GRPC == nil { + cfg.GRPC = &routers.GRPCConfig{ + Network: "tcp", + Address: ":50051", + EnableReflection: true, + EnableHealth: true, + } + } + + return cfg, nil +} diff --git a/api/fx/oracle/internal/server/server.go b/api/fx/oracle/internal/server/server.go new file mode 100644 index 0000000..e1aeb4b --- /dev/null +++ b/api/fx/oracle/internal/server/server.go @@ -0,0 +1,11 @@ +package server + +import ( + serverimp "github.com/tech/sendico/fx/oracle/internal/server/internal" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" +) + +func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return serverimp.Create(logger, file, debug) +} diff --git a/api/fx/oracle/internal/service/oracle/calculator.go b/api/fx/oracle/internal/service/oracle/calculator.go new file mode 100644 index 0000000..599d61c --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/calculator.go @@ -0,0 +1,223 @@ +package oracle + +import ( + "math/big" + "strings" + "time" + + "github.com/google/uuid" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/merrors" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type quoteComputation struct { + pair *model.Pair + rate *model.RateSnapshot + sideProto fxv1.Side + sideModel model.QuoteSide + price *big.Rat + baseInput *big.Rat + quoteInput *big.Rat + amountType model.QuoteAmountType + baseRounded *big.Rat + quoteRounded *big.Rat + priceRounded *big.Rat + baseScale uint32 + quoteScale uint32 + priceScale uint32 + provider string +} + +func newQuoteComputation(pair *model.Pair, rate *model.RateSnapshot, side fxv1.Side, provider string) (*quoteComputation, error) { + if pair == nil || rate == nil { + return nil, merrors.InvalidArgument("oracle: missing pair or rate") + } + sideModel := protoSideToModel(side) + if sideModel == "" { + return nil, merrors.InvalidArgument("oracle: unsupported side") + } + price, err := priceFromRate(rate, side) + if err != nil { + return nil, err + } + if strings.TrimSpace(provider) == "" { + provider = rate.Provider + } + return "eComputation{ + pair: pair, + rate: rate, + sideProto: side, + sideModel: sideModel, + price: price, + baseScale: pair.BaseMeta.Decimals, + quoteScale: pair.QuoteMeta.Decimals, + priceScale: pair.QuoteMeta.Decimals, + provider: provider, + }, nil +} + +func (qc *quoteComputation) withBaseInput(m *moneyv1.Money) error { + if m == nil { + return merrors.InvalidArgument("oracle: base amount missing") + } + if !strings.EqualFold(m.GetCurrency(), qc.pair.Pair.Base) { + return merrors.InvalidArgument("oracle: base amount currency mismatch") + } + val, err := ratFromString(m.GetAmount()) + if err != nil { + return err + } + qc.baseInput = val + qc.amountType = model.QuoteAmountTypeBase + return nil +} + +func (qc *quoteComputation) withQuoteInput(m *moneyv1.Money) error { + if m == nil { + return merrors.InvalidArgument("oracle: quote amount missing") + } + if !strings.EqualFold(m.GetCurrency(), qc.pair.Pair.Quote) { + return merrors.InvalidArgument("oracle: quote amount currency mismatch") + } + val, err := ratFromString(m.GetAmount()) + if err != nil { + return err + } + qc.quoteInput = val + qc.amountType = model.QuoteAmountTypeQuote + return nil +} + +func (qc *quoteComputation) compute() error { + var baseRaw, quoteRaw *big.Rat + switch qc.amountType { + case model.QuoteAmountTypeBase: + baseRaw = qc.baseInput + quoteRaw = mulRat(qc.baseInput, qc.price) + case model.QuoteAmountTypeQuote: + quoteRaw = qc.quoteInput + base, err := divRat(qc.quoteInput, qc.price) + if err != nil { + return err + } + baseRaw = base + default: + return merrors.InvalidArgument("oracle: amount type not set") + } + + var err error + qc.baseRounded, err = roundRatToScale(baseRaw, qc.baseScale, qc.pair.BaseMeta.Rounding) + if err != nil { + return err + } + qc.quoteRounded, err = roundRatToScale(quoteRaw, qc.quoteScale, qc.pair.QuoteMeta.Rounding) + if err != nil { + return err + } + qc.priceRounded, err = roundRatToScale(qc.price, qc.priceScale, qc.pair.QuoteMeta.Rounding) + if err != nil { + return err + } + return nil +} + +func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req *oraclev1.GetQuoteRequest) (*model.Quote, error) { + if qc.baseRounded == nil || qc.quoteRounded == nil || qc.priceRounded == nil { + return nil, merrors.Internal("oracle: computation not executed") + } + + quote := &model.Quote{ + QuoteRef: uuid.NewString(), + Firm: firm, + Status: model.QuoteStatusIssued, + Pair: qc.pair.Pair, + Side: qc.sideModel, + Price: formatRat(qc.priceRounded, qc.priceScale), + BaseAmount: model.Money{ + Currency: qc.pair.Pair.Base, + Amount: formatRat(qc.baseRounded, qc.baseScale), + }, + QuoteAmount: model.Money{ + Currency: qc.pair.Pair.Quote, + Amount: formatRat(qc.quoteRounded, qc.quoteScale), + }, + AmountType: qc.amountType, + RateRef: qc.rate.RateRef, + Provider: qc.provider, + PreferredProvider: req.GetPreferredProvider(), + RequestedTTLMs: req.GetTtlMs(), + MaxAgeToleranceMs: int64(req.GetMaxAgeMs()), + Meta: buildQuoteMeta(req.GetMeta()), + } + + if firm { + quote.ExpiresAtUnixMs = expiryMillis + expiry := time.UnixMilli(expiryMillis) + quote.ExpiresAt = &expiry + } + + return quote, nil +} + +func buildQuoteMeta(meta *oraclev1.RequestMeta) *model.QuoteMeta { + if meta == nil { + return nil + } + trace := meta.GetTrace() + qm := &model.QuoteMeta{ + RequestRef: deriveRequestRef(meta, trace), + TenantRef: meta.GetTenantRef(), + TraceRef: deriveTraceRef(meta, trace), + IdempotencyKey: deriveIdempotencyKey(meta, trace), + } + if org := strings.TrimSpace(meta.GetOrganizationRef()); org != "" { + if objID, err := primitive.ObjectIDFromHex(org); err == nil { + qm.SetOrganizationRef(objID) + } + } + return qm +} + +func protoSideToModel(side fxv1.Side) model.QuoteSide { + switch side { + case fxv1.Side_BUY_BASE_SELL_QUOTE: + return model.QuoteSideBuyBaseSellQuote + case fxv1.Side_SELL_BASE_BUY_QUOTE: + return model.QuoteSideSellBaseBuyQuote + default: + return "" + } +} + +func computeExpiry(now time.Time, ttlMs int64) (int64, error) { + if ttlMs <= 0 { + return 0, merrors.InvalidArgument("oracle: ttl must be positive") + } + return now.Add(time.Duration(ttlMs) * time.Millisecond).UnixMilli(), nil +} + +func deriveRequestRef(meta *oraclev1.RequestMeta, trace *tracev1.TraceContext) string { + if trace != nil && trace.GetRequestRef() != "" { + return trace.GetRequestRef() + } + return meta.GetRequestRef() +} + +func deriveTraceRef(meta *oraclev1.RequestMeta, trace *tracev1.TraceContext) string { + if trace != nil && trace.GetTraceRef() != "" { + return trace.GetTraceRef() + } + return meta.GetTraceRef() +} + +func deriveIdempotencyKey(meta *oraclev1.RequestMeta, trace *tracev1.TraceContext) string { + if trace != nil && trace.GetIdempotencyKey() != "" { + return trace.GetIdempotencyKey() + } + return meta.GetIdempotencyKey() +} diff --git a/api/fx/oracle/internal/service/oracle/cross.go b/api/fx/oracle/internal/service/oracle/cross.go new file mode 100644 index 0000000..10e5d54 --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/cross.go @@ -0,0 +1,221 @@ +package oracle + +import ( + "context" + "fmt" + "math/big" + "strings" + "time" + + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/merrors" +) + +type priceSet struct { + bid *big.Rat + ask *big.Rat + mid *big.Rat +} + +func (s *Service) computeCrossRate(ctx context.Context, pair *model.Pair, provider string) (*model.RateSnapshot, error) { + if pair == nil || pair.Cross == nil || !pair.Cross.Enabled { + return nil, merrors.ErrNoData + } + + baseSnap, err := s.fetchCrossLegSnapshot(ctx, pair.Cross.BaseLeg, provider) + if err != nil { + return nil, err + } + quoteSnap, err := s.fetchCrossLegSnapshot(ctx, pair.Cross.QuoteLeg, provider) + if err != nil { + return nil, err + } + + basePrices, err := buildPriceSet(baseSnap) + if err != nil { + return nil, err + } + quotePrices, err := buildPriceSet(quoteSnap) + if err != nil { + return nil, err + } + + if pair.Cross.BaseLeg.Invert { + basePrices, err = invertPriceSet(basePrices) + if err != nil { + return nil, err + } + } + if pair.Cross.QuoteLeg.Invert { + quotePrices, err = invertPriceSet(quotePrices) + if err != nil { + return nil, err + } + } + + result := multiplyPriceSets(basePrices, quotePrices) + if result.ask.Cmp(result.bid) < 0 { + result.ask, result.bid = result.bid, result.ask + } + + spread := calcSpreadBps(result) + + asOfMs := minNonZero(baseSnap.AsOfUnixMs, quoteSnap.AsOfUnixMs) + if asOfMs == 0 { + asOfMs = time.Now().UnixMilli() + } + asOf := time.UnixMilli(asOfMs) + + rateRef := fmt.Sprintf("cross|%s/%s|%s|%s+%s", pair.Pair.Base, pair.Pair.Quote, provider, baseSnap.RateRef, quoteSnap.RateRef) + + return &model.RateSnapshot{ + RateRef: rateRef, + Pair: pair.Pair, + Provider: provider, + Mid: formatPrice(result.mid), + Bid: formatPrice(result.bid), + Ask: formatPrice(result.ask), + SpreadBps: formatPrice(spread), + AsOfUnixMs: asOfMs, + AsOf: &asOf, + Source: "cross_rate", + ProviderRef: rateRef, + }, nil +} + +func (s *Service) fetchCrossLegSnapshot(ctx context.Context, leg model.CrossRateLeg, fallbackProvider string) (*model.RateSnapshot, error) { + provider := fallbackProvider + if strings.TrimSpace(leg.Provider) != "" { + provider = leg.Provider + } + if provider == "" { + return nil, merrors.InvalidArgument("oracle: cross leg provider missing") + } + return s.storage.Rates().LatestSnapshot(ctx, leg.Pair, provider) +} + +func buildPriceSet(rate *model.RateSnapshot) (priceSet, error) { + if rate == nil { + return priceSet{}, merrors.InvalidArgument("oracle: cross rate requires underlying snapshot") + } + ask, err := parsePrice(rate.Ask) + if err != nil { + return priceSet{}, err + } + bid, err := parsePrice(rate.Bid) + if err != nil { + return priceSet{}, err + } + mid, err := parsePrice(rate.Mid) + if err != nil { + return priceSet{}, err + } + + if ask == nil && bid == nil { + if mid == nil { + return priceSet{}, merrors.InvalidArgument("oracle: cross rate snapshot missing price data") + } + ask = new(big.Rat).Set(mid) + bid = new(big.Rat).Set(mid) + } + if ask == nil && mid != nil { + ask = new(big.Rat).Set(mid) + } + if bid == nil && mid != nil { + bid = new(big.Rat).Set(mid) + } + if ask == nil || bid == nil { + return priceSet{}, merrors.InvalidArgument("oracle: cross rate snapshot missing bid/ask data") + } + + ps := priceSet{ + bid: new(big.Rat).Set(bid), + ask: new(big.Rat).Set(ask), + mid: averageOrMid(bid, ask, mid), + } + if ps.ask.Cmp(ps.bid) < 0 { + ps.ask, ps.bid = ps.bid, ps.ask + } + return ps, nil +} + +func parsePrice(value string) (*big.Rat, error) { + if strings.TrimSpace(value) == "" { + return nil, nil + } + return ratFromString(value) +} + +func averageOrMid(bid, ask, mid *big.Rat) *big.Rat { + if mid != nil { + return new(big.Rat).Set(mid) + } + sum := new(big.Rat).Add(bid, ask) + return sum.Quo(sum, big.NewRat(2, 1)) +} + +func invertPriceSet(ps priceSet) (priceSet, error) { + if ps.ask.Sign() == 0 || ps.bid.Sign() == 0 { + return priceSet{}, merrors.InvalidArgument("oracle: cannot invert zero price") + } + one := big.NewRat(1, 1) + invBid := new(big.Rat).Quo(one, ps.ask) + invAsk := new(big.Rat).Quo(one, ps.bid) + var invMid *big.Rat + if ps.mid != nil && ps.mid.Sign() != 0 { + invMid = new(big.Rat).Quo(one, ps.mid) + } else { + invMid = averageOrMid(invBid, invAsk, nil) + } + result := priceSet{ + bid: invBid, + ask: invAsk, + mid: invMid, + } + if result.ask.Cmp(result.bid) < 0 { + result.ask, result.bid = result.bid, result.ask + } + return result, nil +} + +func multiplyPriceSets(a, b priceSet) priceSet { + result := priceSet{ + bid: mulRat(a.bid, b.bid), + ask: mulRat(a.ask, b.ask), + } + result.mid = averageOrMid(result.bid, result.ask, nil) + return result +} + +func calcSpreadBps(ps priceSet) *big.Rat { + if ps.mid == nil || ps.mid.Sign() == 0 { + return nil + } + spread := new(big.Rat).Sub(ps.ask, ps.bid) + if spread.Sign() < 0 { + spread.Neg(spread) + } + spread.Quo(spread, ps.mid) + spread.Mul(spread, big.NewRat(10000, 1)) + return spread +} + +func minNonZero(values ...int64) int64 { + var result int64 + for _, v := range values { + if v <= 0 { + continue + } + if result == 0 || v < result { + result = v + } + } + return result +} + +func formatPrice(r *big.Rat) string { + if r == nil { + return "" + } + return r.FloatString(8) +} diff --git a/api/fx/oracle/internal/service/oracle/math.go b/api/fx/oracle/internal/service/oracle/math.go new file mode 100644 index 0000000..f3d9822 --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/math.go @@ -0,0 +1,67 @@ +package oracle + +import ( + "math/big" + "strings" + "time" + + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/decimal" + "github.com/tech/sendico/pkg/merrors" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" +) + +// Convenience aliases to pkg/decimal for backward compatibility +var ( + ratFromString = decimal.RatFromString + mulRat = decimal.MulRat + divRat = decimal.DivRat + formatRat = decimal.FormatRat +) + +// roundRatToScale wraps pkg/decimal.RoundRatToScale with model RoundingMode conversion +func roundRatToScale(value *big.Rat, scale uint32, mode model.RoundingMode) (*big.Rat, error) { + return decimal.RoundRatToScale(value, scale, convertRoundingMode(mode)) +} + +// convertRoundingMode converts fx/storage model.RoundingMode to pkg/decimal.RoundingMode +func convertRoundingMode(mode model.RoundingMode) decimal.RoundingMode { + switch mode { + case model.RoundingModeHalfEven: + return decimal.RoundingModeHalfEven + case model.RoundingModeHalfUp: + return decimal.RoundingModeHalfUp + case model.RoundingModeDown: + return decimal.RoundingModeDown + case model.RoundingModeUnspecified: + return decimal.RoundingModeUnspecified + default: + return decimal.RoundingModeHalfEven + } +} + +func priceFromRate(rate *model.RateSnapshot, side fxv1.Side) (*big.Rat, error) { + var priceStr string + switch side { + case fxv1.Side_BUY_BASE_SELL_QUOTE: + priceStr = rate.Ask + case fxv1.Side_SELL_BASE_BUY_QUOTE: + priceStr = rate.Bid + default: + priceStr = "" + } + + if strings.TrimSpace(priceStr) == "" { + priceStr = rate.Mid + } + + if strings.TrimSpace(priceStr) == "" { + return nil, merrors.InvalidArgument("oracle: rate snapshot missing price") + } + + return ratFromString(priceStr) +} + +func timeFromUnixMilli(ms int64) time.Time { + return time.Unix(0, ms*int64(time.Millisecond)) +} diff --git a/api/fx/oracle/internal/service/oracle/metrics.go b/api/fx/oracle/internal/service/oracle/metrics.go new file mode 100644 index 0000000..52152eb --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/metrics.go @@ -0,0 +1,65 @@ +package oracle + +import ( + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +var ( + metricsOnce sync.Once + + rpcRequestsTotal *prometheus.CounterVec + rpcLatency *prometheus.HistogramVec +) + +func initMetrics() { + metricsOnce.Do(func() { + rpcRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Namespace: "fx", + Subsystem: "oracle", + Name: "requests_total", + Help: "Total number of FX oracle RPC calls handled.", + }, + []string{"method", "result"}, + ) + + rpcLatency = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Namespace: "fx", + Subsystem: "oracle", + Name: "request_latency_seconds", + Help: "Latency of FX oracle RPC calls.", + Buckets: prometheus.DefBuckets, + }, + []string{"method", "result"}, + ) + }) +} + +func observeRPC(start time.Time, method string, err error) { + result := labelFromError(err) + rpcRequestsTotal.WithLabelValues(method, result).Inc() + rpcLatency.WithLabelValues(method, result).Observe(time.Since(start).Seconds()) +} + +func labelFromError(err error) string { + if err == nil { + return strings.ToLower(codes.OK.String()) + } + st, ok := status.FromError(err) + if !ok { + return "error" + } + code := st.Code() + if code == codes.OK { + return strings.ToLower(code.String()) + } + return strings.ToLower(code.String()) +} diff --git a/api/fx/oracle/internal/service/oracle/service.go b/api/fx/oracle/internal/service/oracle/service.go new file mode 100644 index 0000000..2adba31 --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/service.go @@ -0,0 +1,402 @@ +package oracle + +import ( + "context" + "errors" + "strings" + "time" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + pmessaging "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + "go.uber.org/zap" + "google.golang.org/grpc" +) + +type serviceError string + +func (e serviceError) Error() string { + return string(e) +} + +var ( + errSideRequired = serviceError("oracle: side is required") + errAmountsMutuallyExclusive = serviceError("oracle: exactly one amount must be provided") + errAmountRequired = serviceError("oracle: amount is required") + errQuoteRefRequired = serviceError("oracle: quote_ref is required") + errEmptyRequest = serviceError("oracle: request payload is empty") + errLedgerTxnRefRequired = serviceError("oracle: ledger_txn_ref is required") +) + +type Service struct { + logger mlogger.Logger + storage storage.Repository + producer pmessaging.Producer + oraclev1.UnimplementedOracleServer +} + +func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer) *Service { + initMetrics() + return &Service{ + logger: logger.Named("oracle"), + storage: repo, + producer: prod, + } +} + +func (s *Service) Register(router routers.GRPC) error { + return router.Register(func(reg grpc.ServiceRegistrar) { + oraclev1.RegisterOracleServer(reg, s) + }) +} + +func (s *Service) GetQuote(ctx context.Context, req *oraclev1.GetQuoteRequest) (*oraclev1.GetQuoteResponse, error) { + start := time.Now() + responder := s.getQuoteResponder(ctx, req) + resp, err := responder(ctx) + observeRPC(start, "GetQuote", err) + return resp, err +} + +func (s *Service) ValidateQuote(ctx context.Context, req *oraclev1.ValidateQuoteRequest) (*oraclev1.ValidateQuoteResponse, error) { + start := time.Now() + responder := s.validateQuoteResponder(ctx, req) + resp, err := responder(ctx) + observeRPC(start, "ValidateQuote", err) + return resp, err +} + +func (s *Service) ConsumeQuote(ctx context.Context, req *oraclev1.ConsumeQuoteRequest) (*oraclev1.ConsumeQuoteResponse, error) { + start := time.Now() + responder := s.consumeQuoteResponder(ctx, req) + resp, err := responder(ctx) + observeRPC(start, "ConsumeQuote", err) + return resp, err +} + +func (s *Service) LatestRate(ctx context.Context, req *oraclev1.LatestRateRequest) (*oraclev1.LatestRateResponse, error) { + start := time.Now() + responder := s.latestRateResponder(ctx, req) + resp, err := responder(ctx) + observeRPC(start, "LatestRate", err) + return resp, err +} + +func (s *Service) ListPairs(ctx context.Context, req *oraclev1.ListPairsRequest) (*oraclev1.ListPairsResponse, error) { + start := time.Now() + responder := s.listPairsResponder(ctx, req) + resp, err := responder(ctx) + observeRPC(start, "ListPairs", err) + return resp, err +} + +func (s *Service) getQuoteResponder(ctx context.Context, req *oraclev1.GetQuoteRequest) gsresponse.Responder[oraclev1.GetQuoteResponse] { + if req == nil { + req = &oraclev1.GetQuoteRequest{} + } + s.logger.Debug("Handling GetQuote", zap.String("pair", req.GetPair().GetBase()+"/"+req.GetPair().GetQuote()), zap.Bool("firm", req.GetFirm())) + if req.GetSide() == fxv1.Side_SIDE_UNSPECIFIED { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errSideRequired) + } + if req.GetBaseAmount() != nil && req.GetQuoteAmount() != nil { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errAmountsMutuallyExclusive) + } + if req.GetBaseAmount() == nil && req.GetQuoteAmount() == nil { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errAmountRequired) + } + if err := s.pingStorage(ctx); err != nil { + s.logger.Warn("Storage unavailable during GetQuote", zap.Error(err)) + return gsresponse.Unavailable[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + pairMsg := req.GetPair() + if pairMsg == nil || strings.TrimSpace(pairMsg.GetBase()) == "" || strings.TrimSpace(pairMsg.GetQuote()) == "" { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, errEmptyRequest) + } + pairKey := model.CurrencyPair{Base: strings.ToUpper(pairMsg.GetBase()), Quote: strings.ToUpper(pairMsg.GetQuote())} + + pair, err := s.storage.Pairs().Get(ctx, pairKey) + if err != nil { + switch { + case errors.Is(err, merrors.ErrNoData): + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, merrors.InvalidArgument("pair_not_supported")) + default: + return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + } + + provider := req.GetPreferredProvider() + if provider == "" { + provider = pair.DefaultProvider + } + if provider == "" && len(pair.Providers) > 0 { + provider = pair.Providers[0] + } + + rate, err := s.getLatestRate(ctx, pair, provider) + if err != nil { + switch { + case errors.Is(err, merrors.ErrNoData): + return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "rate_not_found", err) + default: + return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + } + + now := time.Now() + if maxAge := req.GetMaxAgeMs(); maxAge > 0 { + age := now.UnixMilli() - rate.AsOfUnixMs + if age > int64(maxAge) { + s.logger.Warn("Rate snapshot stale", zap.Int64("age_ms", age), zap.Int32("max_age_ms", req.GetMaxAgeMs()), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", provider)) + return gsresponse.FailedPrecondition[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, "stale_rate", merrors.InvalidArgument("rate older than allowed window")) + } + } + + comp, err := newQuoteComputation(pair, rate, req.GetSide(), provider) + if err != nil { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + + if req.GetBaseAmount() != nil { + if err := comp.withBaseInput(req.GetBaseAmount()); err != nil { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + } else if req.GetQuoteAmount() != nil { + if err := comp.withQuoteInput(req.GetQuoteAmount()); err != nil { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + } + + if err := comp.compute(); err != nil { + return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + + expiresAt := int64(0) + if req.GetFirm() { + expiry, err := computeExpiry(now, req.GetTtlMs()) + if err != nil { + return gsresponse.InvalidArgument[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + expiresAt = expiry + } + + quoteModel, err := comp.buildModelQuote(req.GetFirm(), expiresAt, req) + if err != nil { + return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + + if req.GetFirm() { + if err := s.storage.Quotes().Issue(ctx, quoteModel); err != nil { + switch { + case errors.Is(err, merrors.ErrDataConflict): + return gsresponse.Conflict[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + default: + return gsresponse.Internal[oraclev1.GetQuoteResponse](s.logger, mservice.FXOracle, err) + } + } + s.logger.Info("Firm quote stored", zap.String("quote_ref", quoteModel.QuoteRef), zap.String("pair", pairKey.Base+"/"+pairKey.Quote), zap.String("provider", quoteModel.Provider), zap.Int64("expires_at_ms", quoteModel.ExpiresAtUnixMs)) + } + + resp := &oraclev1.GetQuoteResponse{ + Meta: buildResponseMeta(req.GetMeta()), + Quote: quoteModelToProto(quoteModel), + } + return gsresponse.Success(resp) +} + +func (s *Service) validateQuoteResponder(ctx context.Context, req *oraclev1.ValidateQuoteRequest) gsresponse.Responder[oraclev1.ValidateQuoteResponse] { + if req == nil { + req = &oraclev1.ValidateQuoteRequest{} + } + s.logger.Debug("Handling ValidateQuote", zap.String("quote_ref", req.GetQuoteRef())) + if req.GetQuoteRef() == "" { + return gsresponse.InvalidArgument[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, errQuoteRefRequired) + } + if err := s.pingStorage(ctx); err != nil { + s.logger.Warn("Storage unavailable during ValidateQuote", zap.Error(err)) + return gsresponse.Unavailable[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, err) + } + quote, err := s.storage.Quotes().GetByRef(ctx, req.GetQuoteRef()) + if err != nil { + switch { + case errors.Is(err, merrors.ErrNoData): + resp := &oraclev1.ValidateQuoteResponse{ + Meta: buildResponseMeta(req.GetMeta()), + Quote: nil, + Valid: false, + Reason: "not_found", + } + return gsresponse.Success(resp) + default: + return gsresponse.Internal[oraclev1.ValidateQuoteResponse](s.logger, mservice.FXOracle, err) + } + } + + now := time.Now() + valid := true + reason := "" + if quote.IsExpired(now) { + valid = false + reason = "expired" + } else if quote.Status == model.QuoteStatusConsumed { + valid = false + reason = "consumed" + } + + resp := &oraclev1.ValidateQuoteResponse{ + Meta: buildResponseMeta(req.GetMeta()), + Quote: quoteModelToProto(quote), + Valid: valid, + Reason: reason, + } + return gsresponse.Success(resp) +} + +func (s *Service) consumeQuoteResponder(ctx context.Context, req *oraclev1.ConsumeQuoteRequest) gsresponse.Responder[oraclev1.ConsumeQuoteResponse] { + if req == nil { + req = &oraclev1.ConsumeQuoteRequest{} + } + s.logger.Debug("Handling ConsumeQuote", zap.String("quote_ref", req.GetQuoteRef()), zap.String("ledger_txn_ref", req.GetLedgerTxnRef())) + if req.GetQuoteRef() == "" { + return gsresponse.InvalidArgument[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, errQuoteRefRequired) + } + if req.GetLedgerTxnRef() == "" { + return gsresponse.InvalidArgument[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, errLedgerTxnRefRequired) + } + if err := s.pingStorage(ctx); err != nil { + s.logger.Warn("Storage unavailable during ConsumeQuote", zap.Error(err)) + return gsresponse.Unavailable[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err) + } + _, err := s.storage.Quotes().Consume(ctx, req.GetQuoteRef(), req.GetLedgerTxnRef(), time.Now()) + if err != nil { + switch { + case errors.Is(err, storage.ErrQuoteExpired): + return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "expired", err) + case errors.Is(err, storage.ErrQuoteConsumed): + return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "consumed", err) + case errors.Is(err, storage.ErrQuoteNotFirm): + return gsresponse.FailedPrecondition[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, "not_firm", err) + case errors.Is(err, merrors.ErrNoData): + return gsresponse.NotFound[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err) + default: + return gsresponse.Internal[oraclev1.ConsumeQuoteResponse](s.logger, mservice.FXOracle, err) + } + } + + resp := &oraclev1.ConsumeQuoteResponse{ + Meta: buildResponseMeta(req.GetMeta()), + Consumed: true, + Reason: "consumed", + } + s.logger.Debug("Quote consumed", zap.String("quote_ref", req.GetQuoteRef()), zap.String("ledger_txn_ref", req.GetLedgerTxnRef())) + return gsresponse.Success(resp) +} + +func (s *Service) latestRateResponder(ctx context.Context, req *oraclev1.LatestRateRequest) gsresponse.Responder[oraclev1.LatestRateResponse] { + if req == nil { + req = &oraclev1.LatestRateRequest{} + } + s.logger.Debug("Handling LatestRate", zap.String("pair", req.GetPair().GetBase()+"/"+req.GetPair().GetQuote())) + if err := s.pingStorage(ctx); err != nil { + s.logger.Warn("Storage unavailable during LatestRate", zap.Error(err)) + return gsresponse.Unavailable[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err) + } + pairMsg := req.GetPair() + if pairMsg == nil || strings.TrimSpace(pairMsg.GetBase()) == "" || strings.TrimSpace(pairMsg.GetQuote()) == "" { + return gsresponse.InvalidArgument[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, errEmptyRequest) + } + pair := model.CurrencyPair{Base: strings.ToUpper(pairMsg.GetBase()), Quote: strings.ToUpper(pairMsg.GetQuote())} + + pairMeta, err := s.storage.Pairs().Get(ctx, pair) + if err != nil { + switch { + case errors.Is(err, merrors.ErrNoData): + return gsresponse.NotFound[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err) + default: + return gsresponse.Internal[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err) + } + } + + provider := req.GetProvider() + if provider == "" { + provider = pairMeta.DefaultProvider + } + if provider == "" && len(pairMeta.Providers) > 0 { + provider = pairMeta.Providers[0] + } + + rate, err := s.getLatestRate(ctx, pairMeta, provider) + if err != nil { + switch { + case errors.Is(err, merrors.ErrNoData): + return gsresponse.NotFound[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err) + default: + return gsresponse.Internal[oraclev1.LatestRateResponse](s.logger, mservice.FXOracle, err) + } + } + + resp := &oraclev1.LatestRateResponse{ + Meta: buildResponseMeta(req.GetMeta()), + Rate: rateModelToProto(rate), + } + return gsresponse.Success(resp) +} + +func (s *Service) listPairsResponder(ctx context.Context, req *oraclev1.ListPairsRequest) gsresponse.Responder[oraclev1.ListPairsResponse] { + if req == nil { + req = &oraclev1.ListPairsRequest{} + } + s.logger.Debug("Handling ListPairs") + if err := s.pingStorage(ctx); err != nil { + s.logger.Warn("Storage unavailable during ListPairs", zap.Error(err)) + return gsresponse.Unavailable[oraclev1.ListPairsResponse](s.logger, mservice.FXOracle, err) + } + pairs, err := s.storage.Pairs().ListEnabled(ctx) + if err != nil { + return gsresponse.Internal[oraclev1.ListPairsResponse](s.logger, mservice.FXOracle, err) + } + result := make([]*oraclev1.PairMeta, 0, len(pairs)) + for _, pair := range pairs { + result = append(result, pairModelToProto(pair)) + } + resp := &oraclev1.ListPairsResponse{ + Meta: buildResponseMeta(req.GetMeta()), + Pairs: result, + } + s.logger.Debug("ListPairs returning metadata", zap.Int("pairs", len(resp.GetPairs()))) + return gsresponse.Success(resp) +} + +func (s *Service) pingStorage(ctx context.Context) error { + if s.storage == nil { + return nil + } + return s.storage.Ping(ctx) +} + +func (s *Service) getLatestRate(ctx context.Context, pair *model.Pair, provider string) (*model.RateSnapshot, error) { + rate, err := s.storage.Rates().LatestSnapshot(ctx, pair.Pair, provider) + if err == nil { + return rate, nil + } + if !errors.Is(err, merrors.ErrNoData) { + return nil, err + } + crossRate, crossErr := s.computeCrossRate(ctx, pair, provider) + if crossErr != nil { + if errors.Is(crossErr, merrors.ErrNoData) { + return nil, err + } + return nil, crossErr + } + s.logger.Debug("Derived cross rate", zap.String("pair", pair.Pair.Base+"/"+pair.Pair.Quote), zap.String("provider", provider)) + return crossRate, nil +} + +var _ oraclev1.OracleServer = (*Service)(nil) diff --git a/api/fx/oracle/internal/service/oracle/service_test.go b/api/fx/oracle/internal/service/oracle/service_test.go new file mode 100644 index 0000000..00497b0 --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/service_test.go @@ -0,0 +1,467 @@ +package oracle + +import ( + "context" + "strings" + "testing" + "time" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/merrors" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + "go.uber.org/zap" +) + +type repositoryStub struct { + rates storage.RatesStore + quotes storage.QuotesStore + pairs storage.PairStore + currencies storage.CurrencyStore + pingErr error +} + +func (r *repositoryStub) Ping(ctx context.Context) error { return r.pingErr } +func (r *repositoryStub) Rates() storage.RatesStore { return r.rates } +func (r *repositoryStub) Quotes() storage.QuotesStore { return r.quotes } +func (r *repositoryStub) Pairs() storage.PairStore { return r.pairs } +func (r *repositoryStub) Currencies() storage.CurrencyStore { + return r.currencies +} + +type ratesStoreStub struct { + latestFn func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) +} + +func (r *ratesStoreStub) UpsertSnapshot(ctx context.Context, snapshot *model.RateSnapshot) error { + return nil +} + +func (r *ratesStoreStub) LatestSnapshot(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) { + if r.latestFn != nil { + return r.latestFn(ctx, pair, provider) + } + return nil, merrors.ErrNoData +} + +type quotesStoreStub struct { + issueFn func(ctx context.Context, quote *model.Quote) error + getFn func(ctx context.Context, ref string) (*model.Quote, error) + consumeFn func(ctx context.Context, ref, ledger string, when time.Time) (*model.Quote, error) +} + +func (q *quotesStoreStub) Issue(ctx context.Context, quote *model.Quote) error { + if q.issueFn != nil { + return q.issueFn(ctx, quote) + } + return nil +} + +func (q *quotesStoreStub) GetByRef(ctx context.Context, ref string) (*model.Quote, error) { + if q.getFn != nil { + return q.getFn(ctx, ref) + } + return nil, merrors.ErrNoData +} + +func (q *quotesStoreStub) Consume(ctx context.Context, ref, ledger string, when time.Time) (*model.Quote, error) { + if q.consumeFn != nil { + return q.consumeFn(ctx, ref, ledger, when) + } + return nil, nil +} + +func (q *quotesStoreStub) ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) { + return 0, nil +} + +type pairStoreStub struct { + getFn func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) + listFn func(ctx context.Context) ([]*model.Pair, error) +} + +func (p *pairStoreStub) ListEnabled(ctx context.Context) ([]*model.Pair, error) { + if p.listFn != nil { + return p.listFn(ctx) + } + return nil, nil +} + +func (p *pairStoreStub) Get(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + if p.getFn != nil { + return p.getFn(ctx, pair) + } + return nil, merrors.ErrNoData +} + +func (p *pairStoreStub) Upsert(ctx context.Context, pair *model.Pair) error { return nil } + +type currencyStoreStub struct{} + +func (currencyStoreStub) Get(ctx context.Context, code string) (*model.Currency, error) { + return nil, merrors.ErrNoData +} +func (currencyStoreStub) List(ctx context.Context, codes ...string) ([]*model.Currency, error) { + return nil, nil +} +func (currencyStoreStub) Upsert(ctx context.Context, currency *model.Currency) error { return nil } + +func TestServiceGetQuoteFirm(t *testing.T) { + repo := &repositoryStub{} + repo.pairs = &pairStoreStub{ + getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + return &model.Pair{ + Pair: pair, + BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + }, nil + }, + } + repo.rates = &ratesStoreStub{ + latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) { + return &model.RateSnapshot{ + Pair: pair, + Provider: provider, + Ask: "1.10", + Bid: "1.08", + RateRef: "rate#1", + AsOfUnixMs: time.Now().UnixMilli(), + }, nil + }, + } + savedQuote := &model.Quote{} + repo.quotes = "esStoreStub{ + issueFn: func(ctx context.Context, quote *model.Quote) error { + *savedQuote = *quote + return nil + }, + } + repo.currencies = currencyStoreStub{} + + svc := NewService(zap.NewNop(), repo, nil) + + req := &oraclev1.GetQuoteRequest{ + Meta: &oraclev1.RequestMeta{ + TenantRef: "tenant", + Trace: &tracev1.TraceContext{RequestRef: "req"}, + }, + Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}, + Side: fxv1.Side_BUY_BASE_SELL_QUOTE, + AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{ + Currency: "USD", + Amount: "100", + }}, + Firm: true, + TtlMs: 60000, + } + + resp, err := svc.GetQuote(context.Background(), req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.GetQuote().GetFirm() != true { + t.Fatalf("expected firm quote") + } + if resp.GetQuote().GetQuoteAmount().GetAmount() != "110.00" { + t.Fatalf("unexpected quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount()) + } + if savedQuote.QuoteRef == "" { + t.Fatalf("expected quote persisted") + } +} + +func TestServiceGetQuoteRateNotFound(t *testing.T) { + repo := &repositoryStub{ + pairs: &pairStoreStub{ + getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + return &model.Pair{ + Pair: pair, + BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + }, nil + }, + }, + rates: &ratesStoreStub{latestFn: func(context.Context, model.CurrencyPair, string) (*model.RateSnapshot, error) { + return nil, merrors.ErrNoData + }}, + } + svc := NewService(zap.NewNop(), repo, nil) + + _, err := svc.GetQuote(context.Background(), &oraclev1.GetQuoteRequest{ + Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}, + Side: fxv1.Side_BUY_BASE_SELL_QUOTE, + AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{Currency: "USD", Amount: "1"}}, + }) + if err == nil { + t.Fatalf("expected error") + } +} + +func TestServiceGetQuoteCrossRate(t *testing.T) { + repo := &repositoryStub{} + targetPair := model.CurrencyPair{Base: "EUR", Quote: "RUB"} + baseLegPair := model.CurrencyPair{Base: "USDT", Quote: "EUR"} + quoteLegPair := model.CurrencyPair{Base: "USDT", Quote: "RUB"} + + repo.pairs = &pairStoreStub{ + getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + if pair != targetPair { + t.Fatalf("unexpected pair lookup: %v", pair) + } + return &model.Pair{ + Pair: pair, + BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + DefaultProvider: "CROSSPROV", + Cross: &model.CrossRateConfig{ + Enabled: true, + BaseLeg: model.CrossRateLeg{ + Pair: baseLegPair, + Invert: true, + }, + QuoteLeg: model.CrossRateLeg{ + Pair: quoteLegPair, + }, + }, + }, nil + }, + } + repo.rates = &ratesStoreStub{ + latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) { + switch pair { + case targetPair: + return nil, merrors.ErrNoData + case baseLegPair: + return &model.RateSnapshot{ + Pair: pair, + Provider: provider, + Ask: "0.90", + Bid: "0.90", + Mid: "0.90", + RateRef: "base-leg", + AsOfUnixMs: 1_000, + }, nil + case quoteLegPair: + return &model.RateSnapshot{ + Pair: pair, + Provider: provider, + Ask: "90", + Bid: "90", + Mid: "90", + RateRef: "quote-leg", + AsOfUnixMs: 2_000, + }, nil + default: + return nil, merrors.ErrNoData + } + }, + } + repo.quotes = "esStoreStub{} + repo.currencies = currencyStoreStub{} + + svc := NewService(zap.NewNop(), repo, nil) + + req := &oraclev1.GetQuoteRequest{ + Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "RUB"}, + Side: fxv1.Side_BUY_BASE_SELL_QUOTE, + AmountInput: &oraclev1.GetQuoteRequest_BaseAmount{BaseAmount: &moneyv1.Money{Currency: "EUR", Amount: "1"}}, + } + + resp, err := svc.GetQuote(context.Background(), req) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.GetQuote().GetPrice().GetValue() != "100.00" { + t.Fatalf("unexpected cross price: %s", resp.GetQuote().GetPrice().GetValue()) + } + if resp.GetQuote().GetQuoteAmount().GetAmount() != "100.00" { + t.Fatalf("unexpected cross quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount()) + } + if !strings.HasPrefix(resp.GetQuote().GetRateRef(), "cross|") { + t.Fatalf("expected cross rate ref, got %s", resp.GetQuote().GetRateRef()) + } + if resp.GetQuote().GetProvider() != "CROSSPROV" { + t.Fatalf("unexpected provider: %s", resp.GetQuote().GetProvider()) + } + +} + +func TestServiceLatestRateCross(t *testing.T) { + repo := &repositoryStub{} + targetPair := model.CurrencyPair{Base: "EUR", Quote: "RUB"} + baseLegPair := model.CurrencyPair{Base: "USDT", Quote: "EUR"} + quoteLegPair := model.CurrencyPair{Base: "USDT", Quote: "RUB"} + + repo.pairs = &pairStoreStub{ + getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + if pair != targetPair { + t.Fatalf("unexpected pair lookup: %v", pair) + } + return &model.Pair{ + Pair: pair, + BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + DefaultProvider: "CROSSPROV", + Cross: &model.CrossRateConfig{ + Enabled: true, + BaseLeg: model.CrossRateLeg{ + Pair: baseLegPair, + Invert: true, + }, + QuoteLeg: model.CrossRateLeg{ + Pair: quoteLegPair, + }, + }, + }, nil + }, + } + repo.rates = &ratesStoreStub{ + latestFn: func(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) { + switch pair { + case targetPair: + return nil, merrors.ErrNoData + case baseLegPair: + return &model.RateSnapshot{ + Pair: pair, + Provider: provider, + Ask: "0.90", + Bid: "0.90", + Mid: "0.90", + RateRef: "base-leg", + AsOfUnixMs: 1_000, + }, nil + case quoteLegPair: + return &model.RateSnapshot{ + Pair: pair, + Provider: provider, + Ask: "90", + Bid: "90", + Mid: "90", + RateRef: "quote-leg", + AsOfUnixMs: 2_000, + }, nil + default: + return nil, merrors.ErrNoData + } + }, + } + repo.quotes = "esStoreStub{} + repo.currencies = currencyStoreStub{} + + svc := NewService(zap.NewNop(), repo, nil) + + resp, err := svc.LatestRate(context.Background(), &oraclev1.LatestRateRequest{ + Pair: &fxv1.CurrencyPair{Base: "EUR", Quote: "RUB"}, + }) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + + if resp.GetRate().GetMid().GetValue() != "100.00000000" { + t.Fatalf("unexpected mid price: %s", resp.GetRate().GetMid().GetValue()) + } + if resp.GetRate().GetProvider() != "CROSSPROV" { + t.Fatalf("unexpected provider: %s", resp.GetRate().GetProvider()) + } + if !strings.HasPrefix(resp.GetRate().GetRateRef(), "cross|") { + t.Fatalf("expected cross rate ref, got %s", resp.GetRate().GetRateRef()) + } +} + +func TestServiceValidateQuote(t *testing.T) { + now := time.Now().Add(time.Minute) + repo := &repositoryStub{ + quotes: "esStoreStub{ + getFn: func(context.Context, string) (*model.Quote, error) { + return &model.Quote{ + QuoteRef: "q1", + Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}, + Side: model.QuoteSideBuyBaseSellQuote, + Price: "1.10", + BaseAmount: model.Money{Currency: "USD", Amount: "100"}, + QuoteAmount: model.Money{Currency: "EUR", Amount: "110"}, + ExpiresAtUnixMs: now.UnixMilli(), + Status: model.QuoteStatusIssued, + }, nil + }, + }, + } + svc := NewService(zap.NewNop(), repo, nil) + + resp, err := svc.ValidateQuote(context.Background(), &oraclev1.ValidateQuoteRequest{QuoteRef: "q1"}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !resp.GetValid() { + t.Fatalf("expected quote valid") + } +} + +func TestServiceConsumeQuoteExpired(t *testing.T) { + repo := &repositoryStub{ + quotes: "esStoreStub{ + consumeFn: func(context.Context, string, string, time.Time) (*model.Quote, error) { + return nil, storage.ErrQuoteExpired + }, + }, + } + svc := NewService(zap.NewNop(), repo, nil) + + _, err := svc.ConsumeQuote(context.Background(), &oraclev1.ConsumeQuoteRequest{QuoteRef: "q1", LedgerTxnRef: "ledger"}) + if err == nil { + t.Fatalf("expected error") + } +} + +func TestServiceLatestRateSuccess(t *testing.T) { + repo := &repositoryStub{ + rates: &ratesStoreStub{latestFn: func(_ context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) { + if pair != (model.CurrencyPair{Base: "USD", Quote: "EUR"}) { + t.Fatalf("unexpected pair: %v", pair) + } + if provider != "DEFAULT" { + t.Fatalf("unexpected provider: %s", provider) + } + return &model.RateSnapshot{Pair: pair, RateRef: "rate", Provider: provider}, nil + }}, + pairs: &pairStoreStub{ + getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + return &model.Pair{ + Pair: pair, + BaseMeta: model.CurrencySettings{Code: pair.Base, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + QuoteMeta: model.CurrencySettings{Code: pair.Quote, Decimals: 2, Rounding: model.RoundingModeHalfEven}, + DefaultProvider: "DEFAULT", + }, nil + }, + }, + } + svc := NewService(zap.NewNop(), repo, nil) + + resp, err := svc.LatestRate(context.Background(), &oraclev1.LatestRateRequest{Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if resp.GetRate().GetRateRef() != "rate" { + t.Fatalf("unexpected rate ref") + } +} + +func TestServiceListPairs(t *testing.T) { + repo := &repositoryStub{ + pairs: &pairStoreStub{listFn: func(context.Context) ([]*model.Pair, error) { + return []*model.Pair{{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}}, nil + }}, + } + svc := NewService(zap.NewNop(), repo, nil) + + resp, err := svc.ListPairs(context.Background(), &oraclev1.ListPairsRequest{}) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(resp.GetPairs()) != 1 { + t.Fatalf("expected one pair") + } +} diff --git a/api/fx/oracle/internal/service/oracle/transform.go b/api/fx/oracle/internal/service/oracle/transform.go new file mode 100644 index 0000000..a505a0c --- /dev/null +++ b/api/fx/oracle/internal/service/oracle/transform.go @@ -0,0 +1,126 @@ +package oracle + +import ( + "strings" + + "github.com/tech/sendico/fx/storage/model" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" +) + +func buildResponseMeta(meta *oraclev1.RequestMeta) *oraclev1.ResponseMeta { + resp := &oraclev1.ResponseMeta{} + if meta == nil { + return resp + } + resp.RequestRef = meta.GetRequestRef() + resp.TraceRef = meta.GetTraceRef() + + trace := meta.GetTrace() + if trace == nil { + trace = &tracev1.TraceContext{ + RequestRef: meta.GetRequestRef(), + IdempotencyKey: meta.GetIdempotencyKey(), + TraceRef: meta.GetTraceRef(), + } + } + resp.Trace = trace + return resp +} + +func quoteModelToProto(q *model.Quote) *oraclev1.Quote { + if q == nil { + return nil + } + + return &oraclev1.Quote{ + QuoteRef: q.QuoteRef, + Pair: &fxv1.CurrencyPair{Base: q.Pair.Base, Quote: q.Pair.Quote}, + Side: sideModelToProto(q.Side), + Price: decimalStringToProto(q.Price), + BaseAmount: moneyModelToProto(&q.BaseAmount), + QuoteAmount: moneyModelToProto(&q.QuoteAmount), + ExpiresAtUnixMs: q.ExpiresAtUnixMs, + Provider: q.Provider, + RateRef: q.RateRef, + Firm: q.Firm, + } +} + +func moneyModelToProto(m *model.Money) *moneyv1.Money { + if m == nil { + return nil + } + return &moneyv1.Money{Currency: m.Currency, Amount: m.Amount} +} + +func sideModelToProto(side model.QuoteSide) fxv1.Side { + switch side { + case model.QuoteSideBuyBaseSellQuote: + return fxv1.Side_BUY_BASE_SELL_QUOTE + case model.QuoteSideSellBaseBuyQuote: + return fxv1.Side_SELL_BASE_BUY_QUOTE + default: + return fxv1.Side_SIDE_UNSPECIFIED + } +} + +func rateModelToProto(rate *model.RateSnapshot) *oraclev1.RateSnapshot { + if rate == nil { + return nil + } + return &oraclev1.RateSnapshot{ + Pair: &fxv1.CurrencyPair{Base: rate.Pair.Base, Quote: rate.Pair.Quote}, + Mid: decimalStringToProto(rate.Mid), + Bid: decimalStringToProto(rate.Bid), + Ask: decimalStringToProto(rate.Ask), + AsofUnixMs: rate.AsOfUnixMs, + Provider: rate.Provider, + RateRef: rate.RateRef, + SpreadBps: decimalStringToProto(rate.SpreadBps), + } +} + +func pairModelToProto(pair *model.Pair) *oraclev1.PairMeta { + if pair == nil { + return nil + } + return &oraclev1.PairMeta{ + Pair: &fxv1.CurrencyPair{Base: pair.Pair.Base, Quote: pair.Pair.Quote}, + BaseMeta: currencySettingsToProto(&pair.BaseMeta), + QuoteMeta: currencySettingsToProto(&pair.QuoteMeta), + } +} + +func currencySettingsToProto(c *model.CurrencySettings) *moneyv1.CurrencyMeta { + if c == nil { + return nil + } + return &moneyv1.CurrencyMeta{ + Code: c.Code, + Decimals: c.Decimals, + Rounding: roundingModeToProto(c.Rounding), + } +} + +func roundingModeToProto(mode model.RoundingMode) moneyv1.RoundingMode { + switch mode { + case model.RoundingModeHalfUp: + return moneyv1.RoundingMode_ROUND_HALF_UP + case model.RoundingModeDown: + return moneyv1.RoundingMode_ROUND_DOWN + case model.RoundingModeHalfEven, model.RoundingModeUnspecified: + return moneyv1.RoundingMode_ROUND_HALF_EVEN + default: + return moneyv1.RoundingMode_ROUNDING_MODE_UNSPECIFIED + } +} + +func decimalStringToProto(value string) *moneyv1.Decimal { + if strings.TrimSpace(value) == "" { + return nil + } + return &moneyv1.Decimal{Value: value} +} diff --git a/api/fx/oracle/main.go b/api/fx/oracle/main.go new file mode 100644 index 0000000..cc1e06f --- /dev/null +++ b/api/fx/oracle/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/tech/sendico/fx/oracle/internal/appversion" + si "github.com/tech/sendico/fx/oracle/internal/server" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" + smain "github.com/tech/sendico/pkg/server/main" +) + +func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return si.Create(logger, file, debug) +} + +func main() { + smain.RunServer("main", appversion.Create(), factory) +} diff --git a/api/fx/storage/.gitignore b/api/fx/storage/.gitignore new file mode 100644 index 0000000..f2e5266 --- /dev/null +++ b/api/fx/storage/.gitignore @@ -0,0 +1,2 @@ +internal/generated +.gocache \ No newline at end of file diff --git a/api/fx/storage/go.mod b/api/fx/storage/go.mod new file mode 100644 index 0000000..38dddfe --- /dev/null +++ b/api/fx/storage/go.mod @@ -0,0 +1,32 @@ +module github.com/tech/sendico/fx/storage + +go 1.25.3 + +replace github.com/tech/sendico/pkg => ../../pkg + +require ( + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver v1.17.6 + go.uber.org/zap v1.27.0 +) + +require ( + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.128.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/multierr v1.11.0 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/protobuf v1.36.10 // indirect +) diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum new file mode 100644 index 0000000..da414fc --- /dev/null +++ b/api/fx/storage/go.sum @@ -0,0 +1,177 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +github.com/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.128.0 h1:761dLmXLy/ZNSckAITvpUZ8VdrxARyIlwmdafHzRb7Y= +github.com/casbin/casbin/v2 v2.128.0/go.mod h1:iAwqzcYzJtAK5QWGT2uRl9WfRxXyKFBG1AZuhk2NAQg= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/containerd/log v0.1.0 h1:TCJt7ioM2cr/tfR8GPbGf9/VRAX8D2B4PjzCpfX540I= +github.com/containerd/log v0.1.0/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +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/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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +github.com/morikuni/aec v1.0.0/go.mod h1:BbKIizmSmc5MMPqRYbxO4ZU0S0+P200+tUnFx7PXmsc= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.4 h1:jUorfmVzljjr0FLzYQsGP8cgN/qzzxlY9Vh0C9KFXVw= +go.mongodb.org/mongo-driver v1.17.4/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +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.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/fx/storage/model/cross.go b/api/fx/storage/model/cross.go new file mode 100644 index 0000000..d48f2ef --- /dev/null +++ b/api/fx/storage/model/cross.go @@ -0,0 +1,18 @@ +package model + +// CrossRateConfig describes how to synthetically derive a currency pair using +// two other pairs connected by a pivot currency. +type CrossRateConfig struct { + Enabled bool `bson:"enabled" json:"enabled"` + PivotCurrency string `bson:"pivotCurrency,omitempty" json:"pivotCurrency,omitempty"` + BaseLeg CrossRateLeg `bson:"baseLeg" json:"baseLeg"` + QuoteLeg CrossRateLeg `bson:"quoteLeg" json:"quoteLeg"` +} + +// CrossRateLeg identifies a supporting currency pair and optional overrides to +// fetch or orient its pricing data for cross-rate calculations. +type CrossRateLeg struct { + Pair CurrencyPair `bson:"pair" json:"pair"` + Invert bool `bson:"invert,omitempty" json:"invert,omitempty"` + Provider string `bson:"provider,omitempty" json:"provider,omitempty"` +} diff --git a/api/fx/storage/model/currency.go b/api/fx/storage/model/currency.go new file mode 100644 index 0000000..f905baa --- /dev/null +++ b/api/fx/storage/model/currency.go @@ -0,0 +1,27 @@ +package model + +import "github.com/tech/sendico/pkg/db/storable" + +// Currency captures rounding metadata for a given currency code. +type Currency struct { + storable.Base `bson:",inline" json:",inline"` + + Code string `bson:"code" json:"code"` + Decimals uint32 `bson:"decimals" json:"decimals"` + Rounding RoundingMode `bson:"rounding" json:"rounding"` + DisplayName string `bson:"displayName,omitempty" json:"displayName,omitempty"` + Symbol string `bson:"symbol,omitempty" json:"symbol,omitempty"` + MinUnit string `bson:"minUnit,omitempty" json:"minUnit,omitempty"` +} + +// Collection implements storable.Storable. +func (*Currency) Collection() string { + return CurrenciesCollection +} + +// CurrencySettings embeds precision details inside a Pair document. +type CurrencySettings struct { + Code string `bson:"code" json:"code"` + Decimals uint32 `bson:"decimals" json:"decimals"` + Rounding RoundingMode `bson:"rounding" json:"rounding"` +} diff --git a/api/fx/storage/model/pair.go b/api/fx/storage/model/pair.go new file mode 100644 index 0000000..c9072e2 --- /dev/null +++ b/api/fx/storage/model/pair.go @@ -0,0 +1,26 @@ +package model + +import "github.com/tech/sendico/pkg/db/storable" + +// Pair describes a supported FX currency pair and related metadata. +type Pair struct { + storable.Base `bson:",inline" json:",inline"` + + Pair CurrencyPair `bson:"pair" json:"pair"` + BaseMeta CurrencySettings `bson:"baseMeta" json:"baseMeta"` + QuoteMeta CurrencySettings `bson:"quoteMeta" json:"quoteMeta"` + Providers []string `bson:"providers,omitempty" json:"providers,omitempty"` + IsEnabled bool `bson:"isEnabled" json:"isEnabled"` + TenantRef string `bson:"tenantRef,omitempty" json:"tenantRef,omitempty"` + DefaultProvider string `bson:"defaultProvider,omitempty" json:"defaultProvider,omitempty"` + Attributes map[string]any `bson:"attributes,omitempty" json:"attributes,omitempty"` + SupportedSides []QuoteSide `bson:"supportedSides,omitempty" json:"supportedSides,omitempty"` + FallbackProviders []string `bson:"fallbackProviders,omitempty" json:"fallbackProviders,omitempty"` + Tags []string `bson:"tags,omitempty" json:"tags,omitempty"` + Cross *CrossRateConfig `bson:"cross,omitempty" json:"cross,omitempty"` +} + +// Collection implements storable.Storable. +func (*Pair) Collection() string { + return PairsCollection +} diff --git a/api/fx/storage/model/quote.go b/api/fx/storage/model/quote.go new file mode 100644 index 0000000..c77923b --- /dev/null +++ b/api/fx/storage/model/quote.go @@ -0,0 +1,63 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" +) + +// Quote represents a firm or indicative quote persisted by the oracle. +type Quote struct { + storable.Base `bson:",inline" json:",inline"` + + QuoteRef string `bson:"quoteRef" json:"quoteRef"` + Firm bool `bson:"firm" json:"firm"` + Status QuoteStatus `bson:"status" json:"status"` + Pair CurrencyPair `bson:"pair" json:"pair"` + Side QuoteSide `bson:"side" json:"side"` + Price string `bson:"price" json:"price"` + BaseAmount Money `bson:"baseAmount" json:"baseAmount"` + QuoteAmount Money `bson:"quoteAmount" json:"quoteAmount"` + AmountType QuoteAmountType `bson:"amountType" json:"amountType"` + ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"` + ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"` + RateRef string `bson:"rateRef" json:"rateRef"` + Provider string `bson:"provider" json:"provider"` + PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"` + RequestedTTLMs int64 `bson:"requestedTtlMs,omitempty" json:"requestedTtlMs,omitempty"` + MaxAgeToleranceMs int64 `bson:"maxAgeToleranceMs,omitempty" json:"maxAgeToleranceMs,omitempty"` + ConsumedByLedgerTxnRef string `bson:"consumedByLedgerTxnRef,omitempty" json:"consumedByLedgerTxnRef,omitempty"` + ConsumedAtUnixMs *int64 `bson:"consumedAtUnixMs,omitempty" json:"consumedAtUnixMs,omitempty"` + Meta *QuoteMeta `bson:"meta,omitempty" json:"meta,omitempty"` +} + +// Collection implements storable.Storable. +func (*Quote) Collection() string { + return QuotesCollection +} + +// MarkConsumed switches the quote to consumed status and links it to a ledger transaction. +func (q *Quote) MarkConsumed(ledgerTxnRef string, consumedAt time.Time) { + if ledgerTxnRef == "" { + return + } + q.Status = QuoteStatusConsumed + q.ConsumedByLedgerTxnRef = ledgerTxnRef + ts := consumedAt.UnixMilli() + q.ConsumedAtUnixMs = &ts + q.Base.Update() +} + +// MarkExpired marks the quote as expired. +func (q *Quote) MarkExpired() { + q.Status = QuoteStatusExpired + q.Base.Update() +} + +// IsExpired reports whether the quote has passed its expiration instant. +func (q *Quote) IsExpired(now time.Time) bool { + if q.ExpiresAtUnixMs == 0 { + return false + } + return now.UnixMilli() >= q.ExpiresAtUnixMs +} diff --git a/api/fx/storage/model/rate.go b/api/fx/storage/model/rate.go new file mode 100644 index 0000000..ee674c5 --- /dev/null +++ b/api/fx/storage/model/rate.go @@ -0,0 +1,34 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" +) + +// RateSnapshot stores a normalized FX rate observation. +type RateSnapshot struct { + storable.Base `bson:",inline" json:",inline"` + + RateRef string `bson:"rateRef" json:"rateRef"` + Pair CurrencyPair `bson:"pair" json:"pair"` + Provider string `bson:"provider" json:"provider"` + Mid string `bson:"mid,omitempty" json:"mid,omitempty"` + Bid string `bson:"bid,omitempty" json:"bid,omitempty"` + Ask string `bson:"ask,omitempty" json:"ask,omitempty"` + SpreadBps string `bson:"spreadBps,omitempty" json:"spreadBps,omitempty"` + AsOfUnixMs int64 `bson:"asOfUnixMs" json:"asOfUnixMs"` + AsOf *time.Time `bson:"asOf,omitempty" json:"asOf,omitempty"` + Source string `bson:"source,omitempty" json:"source,omitempty"` + ProviderRef string `bson:"providerRef,omitempty" json:"providerRef,omitempty"` +} + +// Collection implements storable.Storable. +func (*RateSnapshot) Collection() string { + return RatesCollection +} + +// AsOfTime converts the stored millisecond timestamp to time.Time. +func (r *RateSnapshot) AsOfTime() time.Time { + return time.UnixMilli(r.AsOfUnixMs) +} diff --git a/api/fx/storage/model/types.go b/api/fx/storage/model/types.go new file mode 100644 index 0000000..401bb07 --- /dev/null +++ b/api/fx/storage/model/types.go @@ -0,0 +1,68 @@ +package model + +import "github.com/tech/sendico/pkg/model" + +// Collection names used by the FX oracle persistence layer. +const ( + RatesCollection = "rates" + QuotesCollection = "quotes" + CurrenciesCollection = "currencies" + PairsCollection = "pairs" +) + +// QuoteStatus tracks the lifecycle state of a quote. +type QuoteStatus string + +const ( + QuoteStatusIssued QuoteStatus = "issued" + QuoteStatusConsumed QuoteStatus = "consumed" + QuoteStatusExpired QuoteStatus = "expired" +) + +// QuoteSide expresses the trade direction for the requested quote. +type QuoteSide string + +const ( + QuoteSideBuyBaseSellQuote QuoteSide = "buy_base_sell_quote" + QuoteSideSellBaseBuyQuote QuoteSide = "sell_base_buy_quote" +) + +// QuoteAmountType indicates which leg amount was provided by the caller. +type QuoteAmountType string + +const ( + QuoteAmountTypeBase QuoteAmountType = "base" + QuoteAmountTypeQuote QuoteAmountType = "quote" +) + +// RoundingMode describes how rounding should be applied for a currency. +type RoundingMode string + +const ( + RoundingModeUnspecified RoundingMode = "unspecified" + RoundingModeHalfEven RoundingMode = "half_even" + RoundingModeHalfUp RoundingMode = "half_up" + RoundingModeDown RoundingMode = "down" +) + +// CurrencyPair identifies an FX pair. +type CurrencyPair struct { + Base string `bson:"base" json:"base"` + Quote string `bson:"quote" json:"quote"` +} + +// Money represents an exact decimal amount with its currency. +type Money struct { + Currency string `bson:"currency" json:"currency"` + Amount string `bson:"amount" json:"amount"` +} + +// QuoteMeta carries request-scoped metadata associated with a quote. +type QuoteMeta struct { + model.OrganizationBoundBase `bson:",inline" json:",inline"` + + RequestRef string `bson:"requestRef,omitempty" json:"requestRef,omitempty"` + TenantRef string `bson:"tenantRef,omitempty" json:"tenantRef,omitempty"` + TraceRef string `bson:"traceRef,omitempty" json:"traceRef,omitempty"` + IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotencyKey,omitempty"` +} diff --git a/api/fx/storage/mongo/repository.go b/api/fx/storage/mongo/repository.go new file mode 100644 index 0000000..66913f0 --- /dev/null +++ b/api/fx/storage/mongo/repository.go @@ -0,0 +1,115 @@ +package mongo + +import ( + "context" + "time" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/mongo/store" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/db/transaction" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type Store struct { + logger mlogger.Logger + conn *db.MongoConnection + db *mongo.Database + txFactory transaction.Factory + + rates storage.RatesStore + quotes storage.QuotesStore + pairs storage.PairStore + currencies storage.CurrencyStore +} + +func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) { + if conn == nil { + return nil, merrors.InvalidArgument("mongo connection is nil") + } + + client := conn.Client() + if client == nil { + return nil, merrors.Internal("mongo client not initialised") + } + + db := conn.Database() + txFactory := newMongoTransactionFactory(client) + + s := &Store{ + logger: logger.Named("storage").Named("mongo"), + conn: conn, + db: db, + txFactory: txFactory, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := s.Ping(ctx); err != nil { + s.logger.Error("mongo ping failed during store init", zap.Error(err)) + return nil, err + } + + ratesStore, err := store.NewRates(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize rates store", zap.Error(err)) + return nil, err + } + quotesStore, err := store.NewQuotes(s.logger, db, txFactory) + if err != nil { + s.logger.Error("failed to initialize quotes store", zap.Error(err)) + return nil, err + } + pairsStore, err := store.NewPair(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize pair store", zap.Error(err)) + return nil, err + } + currencyStore, err := store.NewCurrency(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize currency store", zap.Error(err)) + return nil, err + } + + s.rates = ratesStore + s.quotes = quotesStore + s.pairs = pairsStore + s.currencies = currencyStore + + s.logger.Info("mongo storage ready") + return s, nil +} + +func (s *Store) Ping(ctx context.Context) error { + return s.conn.Ping(ctx) +} + +func (s *Store) Rates() storage.RatesStore { + return s.rates +} + +func (s *Store) Quotes() storage.QuotesStore { + return s.quotes +} + +func (s *Store) Pairs() storage.PairStore { + return s.pairs +} + +func (s *Store) Currencies() storage.CurrencyStore { + return s.currencies +} + +func (s *Store) Database() *mongo.Database { + return s.db +} + +func (s *Store) TransactionFactory() transaction.Factory { + return s.txFactory +} + +var _ storage.Repository = (*Store)(nil) diff --git a/api/fx/storage/mongo/store/currency.go b/api/fx/storage/mongo/store/currency.go new file mode 100644 index 0000000..8197e2e --- /dev/null +++ b/api/fx/storage/mongo/store/currency.go @@ -0,0 +1,113 @@ +package store + +import ( + "context" + "errors" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type currencyStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewCurrency(logger mlogger.Logger, db *mongo.Database) (storage.CurrencyStore, error) { + repo := repository.CreateMongoRepository(db, model.CurrenciesCollection) + + index := &ri.Definition{ + Keys: []ri.Key{ + {Field: "code", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(index); err != nil { + logger.Error("failed to ensure currencies index", zap.Error(err)) + return nil, err + } + childLogger := logger.Named(model.CurrenciesCollection) + childLogger.Debug("currency store initialised", zap.String("collection", model.CurrenciesCollection)) + + return ¤cyStore{ + logger: childLogger, + repo: repo, + }, nil +} + +func (c *currencyStore) Get(ctx context.Context, code string) (*model.Currency, error) { + if code == "" { + c.logger.Warn("attempt to fetch currency with empty code") + return nil, merrors.InvalidArgument("currencyStore: empty code") + } + result := &model.Currency{} + if err := c.repo.FindOneByFilter(ctx, repository.Filter("code", code), result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + c.logger.Debug("currency not found", zap.String("code", code)) + } + return nil, err + } + c.logger.Debug("currency loaded", zap.String("code", code)) + return result, nil +} + +func (c *currencyStore) List(ctx context.Context, codes ...string) ([]*model.Currency, error) { + query := repository.Query() + if len(codes) > 0 { + values := make([]any, len(codes)) + for i, code := range codes { + values[i] = code + } + query = query.In(repository.Field("code"), values...) + } + + currencies := make([]*model.Currency, 0) + err := c.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.Currency{} + if err := cur.Decode(doc); err != nil { + return err + } + currencies = append(currencies, doc) + return nil + }) + if err != nil { + c.logger.Error("failed to list currencies", zap.Error(err)) + return nil, err + } + c.logger.Debug("listed currencies", zap.Int("count", len(currencies))) + return currencies, nil +} + +func (c *currencyStore) Upsert(ctx context.Context, currency *model.Currency) error { + if currency == nil { + c.logger.Warn("attempt to upsert nil currency") + return merrors.InvalidArgument("currencyStore: nil currency") + } + if currency.Code == "" { + c.logger.Warn("attempt to upsert currency with empty code") + return merrors.InvalidArgument("currencyStore: empty code") + } + + existing := &model.Currency{} + filter := repository.Filter("code", currency.Code) + if err := c.repo.FindOneByFilter(ctx, filter, existing); err != nil { + if errors.Is(err, merrors.ErrNoData) { + c.logger.Debug("inserting new currency", zap.String("code", currency.Code)) + return c.repo.Insert(ctx, currency, filter) + } + c.logger.Error("failed to fetch currency", zap.Error(err), zap.String("code", currency.Code)) + return err + } + + if existing.GetID() != nil { + currency.SetID(*existing.GetID()) + } + c.logger.Debug("updating currency", zap.String("code", currency.Code)) + return c.repo.Update(ctx, currency) +} diff --git a/api/fx/storage/mongo/store/currency_test.go b/api/fx/storage/mongo/store/currency_test.go new file mode 100644 index 0000000..066460f --- /dev/null +++ b/api/fx/storage/mongo/store/currency_test.go @@ -0,0 +1,104 @@ +package store + +import ( + "context" + "errors" + "testing" + + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestCurrencyStoreGet(t *testing.T) { + repo := &repoStub{ + findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error { + currency := result.(*model.Currency) + currency.Code = "USD" + return nil + }, + } + store := ¤cyStore{logger: zap.NewNop(), repo: repo} + + res, err := store.Get(context.Background(), "USD") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.Code != "USD" { + t.Fatalf("unexpected code: %s", res.Code) + } +} + +func TestCurrencyStoreList(t *testing.T) { + repo := &repoStub{ + findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error { + return runDecoderWithDocs(t, decode, &model.Currency{Code: "USD"}) + }, + } + store := ¤cyStore{logger: zap.NewNop(), repo: repo} + + currencies, err := store.List(context.Background(), "USD") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(currencies) != 1 || currencies[0].Code != "USD" { + t.Fatalf("unexpected list result: %+v", currencies) + } +} + +func TestCurrencyStoreUpsertInsert(t *testing.T) { + inserted := false + repo := &repoStub{ + findOneFn: func(context.Context, builder.Query, storable.Storable) error { + return merrors.ErrNoData + }, + insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error { + _ = cloneCurrency(t, obj) + inserted = true + return nil + }, + } + store := ¤cyStore{logger: zap.NewNop(), repo: repo} + + if err := store.Upsert(context.Background(), &model.Currency{Code: "USD"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if !inserted { + t.Fatalf("expected insert to be called") + } +} + +func TestCurrencyStoreGetInvalid(t *testing.T) { + store := ¤cyStore{logger: zap.NewNop(), repo: &repoStub{}} + if _, err := store.Get(context.Background(), ""); !errors.Is(err, merrors.ErrInvalidArg) { + t.Fatalf("expected invalid argument error") + } +} + +func TestCurrencyStoreUpsertUpdate(t *testing.T) { + var updated *model.Currency + repo := &repoStub{ + findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error { + currency := result.(*model.Currency) + currency.SetID(primitive.NewObjectID()) + currency.Code = "USD" + return nil + }, + updateFn: func(_ context.Context, obj storable.Storable) error { + updated = cloneCurrency(t, obj) + return nil + }, + } + store := ¤cyStore{logger: zap.NewNop(), repo: repo} + + if err := store.Upsert(context.Background(), &model.Currency{Code: "USD"}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if updated == nil || updated.GetID() == nil { + t.Fatalf("expected update to preserve ID") + } +} diff --git a/api/fx/storage/mongo/store/pair.go b/api/fx/storage/mongo/store/pair.go new file mode 100644 index 0000000..e6eeb0f --- /dev/null +++ b/api/fx/storage/mongo/store/pair.go @@ -0,0 +1,111 @@ +package store + +import ( + "context" + "errors" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type pairStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewPair(logger mlogger.Logger, db *mongo.Database) (storage.PairStore, error) { + repo := repository.CreateMongoRepository(db, model.PairsCollection) + index := &ri.Definition{ + Keys: []ri.Key{ + {Field: "pair.base", Sort: ri.Asc}, + {Field: "pair.quote", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(index); err != nil { + logger.Error("failed to ensure pairs index", zap.Error(err)) + return nil, err + } + logger.Debug("pair store initialised", zap.String("collection", model.PairsCollection)) + + return &pairStore{ + logger: logger.Named(model.PairsCollection), + repo: repo, + }, nil +} + +func (p *pairStore) ListEnabled(ctx context.Context) ([]*model.Pair, error) { + filter := repository.Query().Filter(repository.Field("isEnabled"), true) + + pairs := make([]*model.Pair, 0) + err := p.repo.FindManyByFilter(ctx, filter, func(cur *mongo.Cursor) error { + doc := &model.Pair{} + if err := cur.Decode(doc); err != nil { + return err + } + pairs = append(pairs, doc) + return nil + }) + if err != nil { + p.logger.Error("failed to list enabled pairs", zap.Error(err)) + return nil, err + } + p.logger.Debug("listed enabled pairs", zap.Int("count", len(pairs))) + return pairs, nil +} + +func (p *pairStore) Get(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { + if pair.Base == "" || pair.Quote == "" { + p.logger.Warn("attempt to fetch pair with empty currency", zap.String("base", pair.Base), zap.String("quote", pair.Quote)) + return nil, merrors.InvalidArgument("pairStore: incomplete pair") + } + result := &model.Pair{} + query := repository.Query(). + Filter(repository.Field("pair").Dot("base"), pair.Base). + Filter(repository.Field("pair").Dot("quote"), pair.Quote) + if err := p.repo.FindOneByFilter(ctx, query, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + p.logger.Debug("pair not found", zap.String("base", pair.Base), zap.String("quote", pair.Quote)) + } + return nil, err + } + p.logger.Debug("pair loaded", zap.String("base", pair.Base), zap.String("quote", pair.Quote)) + return result, nil +} + +func (p *pairStore) Upsert(ctx context.Context, pair *model.Pair) error { + if pair == nil { + p.logger.Warn("attempt to upsert nil pair") + return merrors.InvalidArgument("pairStore: nil pair") + } + if pair.Pair.Base == "" || pair.Pair.Quote == "" { + p.logger.Warn("attempt to upsert pair with empty currency", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote)) + return merrors.InvalidArgument("pairStore: incomplete pair") + } + + existing := &model.Pair{} + query := repository.Query(). + Filter(repository.Field("pair").Dot("base"), pair.Pair.Base). + Filter(repository.Field("pair").Dot("quote"), pair.Pair.Quote) + err := p.repo.FindOneByFilter(ctx, query, existing) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + p.logger.Debug("inserting new pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote)) + return p.repo.Insert(ctx, pair, query) + } + p.logger.Error("failed to fetch pair", zap.Error(err), zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote)) + return err + } + + if existing.GetID() != nil { + pair.SetID(*existing.GetID()) + } + p.logger.Debug("updating pair", zap.String("base", pair.Pair.Base), zap.String("quote", pair.Pair.Quote)) + return p.repo.Update(ctx, pair) +} diff --git a/api/fx/storage/mongo/store/pair_test.go b/api/fx/storage/mongo/store/pair_test.go new file mode 100644 index 0000000..6eda333 --- /dev/null +++ b/api/fx/storage/mongo/store/pair_test.go @@ -0,0 +1,101 @@ +package store + +import ( + "context" + "errors" + "testing" + + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestPairStoreListEnabled(t *testing.T) { + repo := &repoStub{ + findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error { + docs := []interface{}{ + &model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}, + } + return runDecoderWithDocs(t, decode, docs...) + }, + } + store := &pairStore{logger: zap.NewNop(), repo: repo} + + pairs, err := store.ListEnabled(context.Background()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if len(pairs) != 1 || pairs[0].Pair.Base != "USD" { + t.Fatalf("unexpected pairs result: %+v", pairs) + } +} + +func TestPairStoreGetInvalid(t *testing.T) { + store := &pairStore{logger: zap.NewNop(), repo: &repoStub{}} + if _, err := store.Get(context.Background(), model.CurrencyPair{}); !errors.Is(err, merrors.ErrInvalidArg) { + t.Fatalf("expected invalid argument error") + } +} + +func TestPairStoreGetNotFound(t *testing.T) { + repo := &repoStub{ + findOneFn: func(context.Context, builder.Query, storable.Storable) error { + return merrors.ErrNoData + }, + } + store := &pairStore{logger: zap.NewNop(), repo: repo} + + if _, err := store.Get(context.Background(), model.CurrencyPair{Base: "USD", Quote: "EUR"}); !errors.Is(err, merrors.ErrNoData) { + t.Fatalf("expected ErrNoData, got %v", err) + } +} + +func TestPairStoreUpsertInsert(t *testing.T) { + ctx := context.Background() + var inserted *model.Pair + repo := &repoStub{ + findOneFn: func(context.Context, builder.Query, storable.Storable) error { + return merrors.ErrNoData + }, + insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error { + inserted = clonePair(t, obj) + return nil + }, + } + store := &pairStore{logger: zap.NewNop(), repo: repo} + pair := &model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}} + if err := store.Upsert(ctx, pair); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if inserted == nil { + t.Fatalf("expected insert to be called") + } +} + +func TestPairStoreUpsertUpdate(t *testing.T) { + ctx := context.Background() + var updated *model.Pair + repo := &repoStub{ + findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error { + pair := result.(*model.Pair) + pair.SetID(primitive.NewObjectID()) + return nil + }, + updateFn: func(_ context.Context, obj storable.Storable) error { + updated = clonePair(t, obj) + return nil + }, + } + store := &pairStore{logger: zap.NewNop(), repo: repo} + + if err := store.Upsert(ctx, &model.Pair{Pair: model.CurrencyPair{Base: "USD", Quote: "EUR"}}); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if updated == nil || updated.GetID() == nil { + t.Fatalf("expected update to preserve existing ID") + } +} diff --git a/api/fx/storage/mongo/store/quotes.go b/api/fx/storage/mongo/store/quotes.go new file mode 100644 index 0000000..a5edd9d --- /dev/null +++ b/api/fx/storage/mongo/store/quotes.go @@ -0,0 +1,198 @@ +package store + +import ( + "context" + "errors" + "time" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/transaction" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type quotesStore struct { + logger mlogger.Logger + repo repository.Repository + txFactory transaction.Factory +} + +func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.Factory) (storage.QuotesStore, error) { + repo := repository.CreateMongoRepository(db, model.QuotesCollection) + indexes := []*ri.Definition{ + { + Keys: []ri.Key{ + {Field: "quoteRef", Sort: ri.Asc}, + }, + Unique: true, + }, + { + Keys: []ri.Key{ + {Field: "status", Sort: ri.Asc}, + {Field: "expiresAtUnixMs", Sort: ri.Asc}, + }, + }, + { + Keys: []ri.Key{ + {Field: "consumedByLedgerTxnRef", Sort: ri.Asc}, + }, + }, + } + + ttlSeconds := int32(0) + indexes = append(indexes, &ri.Definition{ + Keys: []ri.Key{ + {Field: "expiresAt", Sort: ri.Asc}, + }, + TTL: &ttlSeconds, + Name: "quotes_expires_at_ttl", + }) + + for _, def := range indexes { + if err := repo.CreateIndex(def); err != nil { + logger.Error("failed to ensure quotes index", zap.Error(err)) + return nil, err + } + } + childLogger := logger.Named(model.QuotesCollection) + childLogger.Debug("quotes store initialised", zap.String("collection", model.QuotesCollection)) + + return "esStore{ + logger: childLogger, + repo: repo, + txFactory: txFactory, + }, nil +} + +func (q *quotesStore) Issue(ctx context.Context, quote *model.Quote) error { + if quote == nil { + q.logger.Warn("attempt to issue nil quote") + return merrors.InvalidArgument("quotesStore: nil quote") + } + if quote.QuoteRef == "" { + q.logger.Warn("attempt to issue quote with empty ref") + return merrors.InvalidArgument("quotesStore: empty quoteRef") + } + + if quote.ExpiresAtUnixMs > 0 && quote.ExpiresAt == nil { + expiry := time.UnixMilli(quote.ExpiresAtUnixMs) + quote.ExpiresAt = &expiry + } + + quote.Status = model.QuoteStatusIssued + quote.ConsumedByLedgerTxnRef = "" + quote.ConsumedAtUnixMs = nil + if err := q.repo.Insert(ctx, quote, repository.Filter("quoteRef", quote.QuoteRef)); err != nil { + q.logger.Error("failed to insert quote", zap.Error(err), zap.String("quote_ref", quote.QuoteRef)) + return err + } + q.logger.Debug("quote issued", zap.String("quote_ref", quote.QuoteRef), zap.Bool("firm", quote.Firm)) + return nil +} + +func (q *quotesStore) GetByRef(ctx context.Context, quoteRef string) (*model.Quote, error) { + if quoteRef == "" { + q.logger.Warn("attempt to fetch quote with empty ref") + return nil, merrors.InvalidArgument("quotesStore: empty quoteRef") + } + quote := &model.Quote{} + if err := q.repo.FindOneByFilter(ctx, repository.Filter("quoteRef", quoteRef), quote); err != nil { + if errors.Is(err, merrors.ErrNoData) { + q.logger.Debug("quote not found", zap.String("quote_ref", quoteRef)) + } + return nil, err + } + q.logger.Debug("quote loaded", zap.String("quote_ref", quoteRef), zap.String("status", string(quote.Status))) + return quote, nil +} + +func (q *quotesStore) Consume(ctx context.Context, quoteRef, ledgerTxnRef string, when time.Time) (*model.Quote, error) { + if quoteRef == "" || ledgerTxnRef == "" { + q.logger.Warn("attempt to consume quote with missing identifiers", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef)) + return nil, merrors.InvalidArgument("quotesStore: missing identifiers") + } + + if when.IsZero() { + when = time.Now() + } + + q.logger.Debug("consuming quote", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef)) + txn := q.txFactory.CreateTransaction() + result, err := txn.Execute(ctx, func(txCtx context.Context) (any, error) { + quote := &model.Quote{} + if err := q.repo.FindOneByFilter(txCtx, repository.Filter("quoteRef", quoteRef), quote); err != nil { + return nil, err + } + + if !quote.Firm { + q.logger.Warn("quote not firm", zap.String("quote_ref", quoteRef)) + return nil, storage.ErrQuoteNotFirm + } + + if quote.Status == model.QuoteStatusExpired || quote.IsExpired(when) { + quote.MarkExpired() + if err := q.repo.Update(txCtx, quote); err != nil { + return nil, err + } + q.logger.Info("quote expired during consume", zap.String("quote_ref", quoteRef)) + return nil, storage.ErrQuoteExpired + } + + if quote.Status == model.QuoteStatusConsumed { + if quote.ConsumedByLedgerTxnRef == ledgerTxnRef { + q.logger.Debug("quote already consumed by ledger", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef)) + return quote, nil + } + q.logger.Warn("quote consumed by different ledger", zap.String("quote_ref", quoteRef), zap.String("existing_ledger_ref", quote.ConsumedByLedgerTxnRef)) + return nil, storage.ErrQuoteConsumed + } + + quote.MarkConsumed(ledgerTxnRef, when) + if err := q.repo.Update(txCtx, quote); err != nil { + return nil, err + } + q.logger.Info("quote consumed", zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef)) + return quote, nil + }) + if err != nil { + q.logger.Error("quote consumption failed", zap.Error(err), zap.String("quote_ref", quoteRef), zap.String("ledger_ref", ledgerTxnRef)) + return nil, err + } + quote, _ := result.(*model.Quote) + if quote == nil { + return nil, merrors.Internal("quotesStore: transaction returned nil quote") + } + return quote, nil +} + +func (q *quotesStore) ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) { + if cutoff.IsZero() { + q.logger.Warn("attempt to expire quotes with zero cutoff") + return 0, merrors.InvalidArgument("quotesStore: cutoff time is zero") + } + + filter := repository.Query(). + Filter(repository.Field("status"), model.QuoteStatusIssued). + Comparison(repository.Field("expiresAtUnixMs"), builder.Lt, cutoff.UnixMilli()) + + patch := repository.Patch(). + Set(repository.Field("status"), model.QuoteStatusExpired). + Unset(repository.Field("consumedByLedgerTxnRef")). + Unset(repository.Field("consumedAtUnixMs")) + + updated, err := q.repo.PatchMany(ctx, filter, patch) + if err != nil { + q.logger.Error("failed to expire quotes", zap.Error(err)) + return 0, err + } + if updated > 0 { + q.logger.Info("quotes expired", zap.Int("count", updated)) + } + return updated, nil +} diff --git a/api/fx/storage/mongo/store/quotes_test.go b/api/fx/storage/mongo/store/quotes_test.go new file mode 100644 index 0000000..d65779d --- /dev/null +++ b/api/fx/storage/mongo/store/quotes_test.go @@ -0,0 +1,184 @@ +package store + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "go.uber.org/zap" +) + +func TestQuotesStoreIssue(t *testing.T) { + ctx := context.Background() + var inserted *model.Quote + repo := &repoStub{ + insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error { + inserted = cloneQuote(t, obj) + return nil + }, + } + store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}} + + quote := &model.Quote{QuoteRef: "q1"} + if err := store.Issue(ctx, quote); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if inserted == nil || inserted.Status != model.QuoteStatusIssued { + t.Fatalf("expected issued quote to be inserted") + } +} + +func TestQuotesStoreIssueSetsExpiryDate(t *testing.T) { + ctx := context.Background() + var inserted *model.Quote + repo := &repoStub{ + insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error { + inserted = cloneQuote(t, obj) + return nil + }, + } + store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}} + + expiry := time.Now().Add(2 * time.Minute).UnixMilli() + quote := &model.Quote{ + QuoteRef: "q1", + ExpiresAtUnixMs: expiry, + } + + if err := store.Issue(ctx, quote); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if inserted == nil || inserted.ExpiresAt == nil { + t.Fatalf("expected expiry timestamp to be populated") + } + if inserted.ExpiresAt.UnixMilli() != expiry { + t.Fatalf("expected expiry to equal %d, got %d", expiry, inserted.ExpiresAt.UnixMilli()) + } +} + +func TestQuotesStoreIssueInvalidInput(t *testing.T) { + store := "esStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}} + if err := store.Issue(context.Background(), nil); !errors.Is(err, merrors.ErrInvalidArg) { + t.Fatalf("expected invalid argument error, got %v", err) + } +} + +func TestQuotesStoreConsumeSuccess(t *testing.T) { + ctx := context.Background() + now := time.Now() + ledgerRef := "ledger-1" + + stored := &model.Quote{ + QuoteRef: "q1", + Firm: true, + Status: model.QuoteStatusIssued, + ExpiresAtUnixMs: now.Add(5 * time.Minute).UnixMilli(), + } + var updated *model.Quote + repo := &repoStub{ + findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error { + quote := result.(*model.Quote) + *quote = *stored + return nil + }, + updateFn: func(_ context.Context, obj storable.Storable) error { + updated = cloneQuote(t, obj) + return nil + }, + } + factory := &txFactoryStub{} + store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: factory} + + res, err := store.Consume(ctx, "q1", ledgerRef, now) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res == nil || res.Status != model.QuoteStatusConsumed { + t.Fatalf("expected consumed quote") + } + if updated == nil || updated.ConsumedByLedgerTxnRef != ledgerRef { + t.Fatalf("expected update with ledger ref") + } +} + +func TestQuotesStoreConsumeExpired(t *testing.T) { + ctx := context.Background() + stored := &model.Quote{ + QuoteRef: "q1", + Firm: true, + Status: model.QuoteStatusIssued, + ExpiresAtUnixMs: time.Now().Add(-time.Minute).UnixMilli(), + } + var updated *model.Quote + repo := &repoStub{ + findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error { + quote := result.(*model.Quote) + *quote = *stored + return nil + }, + updateFn: func(_ context.Context, obj storable.Storable) error { + updated = cloneQuote(t, obj) + return nil + }, + } + factory := &txFactoryStub{} + store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: factory} + + _, err := store.Consume(ctx, "q1", "ledger", time.Now()) + if !errors.Is(err, storage.ErrQuoteExpired) { + t.Fatalf("expected ErrQuoteExpired, got %v", err) + } + if updated == nil || updated.Status != model.QuoteStatusExpired { + t.Fatalf("expected quote marked expired") + } +} + +func TestQuotesStoreExpireIssuedBefore(t *testing.T) { + repo := &repoStub{ + patchManyFn: func(context.Context, builder.Query, builder.Patch) (int, error) { + return 3, nil + }, + } + store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}} + + count, err := store.ExpireIssuedBefore(context.Background(), time.Now()) + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if count != 3 { + t.Fatalf("expected 3 expired quotes, got %d", count) + } +} + +func TestQuotesStoreExpireZeroCutoff(t *testing.T) { + store := "esStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}} + if _, err := store.ExpireIssuedBefore(context.Background(), time.Time{}); !errors.Is(err, merrors.ErrInvalidArg) { + t.Fatalf("expected invalid argument error") + } +} + +func TestQuotesStoreGetByRefNotFound(t *testing.T) { + repo := &repoStub{ + findOneFn: func(context.Context, builder.Query, storable.Storable) error { + return merrors.ErrNoData + }, + } + store := "esStore{logger: zap.NewNop(), repo: repo, txFactory: &txFactoryStub{}} + + if _, err := store.GetByRef(context.Background(), "missing"); !errors.Is(err, merrors.ErrNoData) { + t.Fatalf("expected ErrNoData, got %v", err) + } +} + +func TestQuotesStoreGetByRefInvalid(t *testing.T) { + store := "esStore{logger: zap.NewNop(), repo: &repoStub{}, txFactory: &txFactoryStub{}} + if _, err := store.GetByRef(context.Background(), ""); !errors.Is(err, merrors.ErrInvalidArg) { + t.Fatalf("expected invalid argument error") + } +} diff --git a/api/fx/storage/mongo/store/rates.go b/api/fx/storage/mongo/store/rates.go new file mode 100644 index 0000000..a0561b7 --- /dev/null +++ b/api/fx/storage/mongo/store/rates.go @@ -0,0 +1,127 @@ +package store + +import ( + "context" + "errors" + "time" + + "github.com/tech/sendico/fx/storage" + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type ratesStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewRates(logger mlogger.Logger, db *mongo.Database) (storage.RatesStore, error) { + repo := repository.CreateMongoRepository(db, model.RatesCollection) + + indexes := []*ri.Definition{ + { + Keys: []ri.Key{ + {Field: "pair.base", Sort: ri.Asc}, + {Field: "pair.quote", Sort: ri.Asc}, + {Field: "provider", Sort: ri.Asc}, + {Field: "asOfUnixMs", Sort: ri.Desc}, + }, + }, + { + Keys: []ri.Key{ + {Field: "rateRef", Sort: ri.Asc}, + }, + Unique: true, + }, + } + + ttlSeconds := int32(24 * 60 * 60) + indexes = append(indexes, &ri.Definition{ + Keys: []ri.Key{ + {Field: "asOf", Sort: ri.Asc}, + }, + TTL: &ttlSeconds, + Name: "rates_as_of_ttl", + }) + + for _, def := range indexes { + if err := repo.CreateIndex(def); err != nil { + logger.Error("failed to ensure rates index", zap.Error(err)) + return nil, err + } + } + logger.Debug("rates store initialised", zap.String("collection", model.RatesCollection)) + return &ratesStore{ + logger: logger.Named(model.RatesCollection), + repo: repo, + }, nil +} + +func (r *ratesStore) UpsertSnapshot(ctx context.Context, snapshot *model.RateSnapshot) error { + if snapshot == nil { + r.logger.Warn("attempt to upsert nil snapshot") + return merrors.InvalidArgument("ratesStore: nil snapshot") + } + if snapshot.RateRef == "" { + r.logger.Warn("attempt to upsert snapshot with empty rate_ref") + return merrors.InvalidArgument("ratesStore: empty rateRef") + } + + if snapshot.AsOfUnixMs > 0 && snapshot.AsOf == nil { + asOf := time.UnixMilli(snapshot.AsOfUnixMs).UTC() + snapshot.AsOf = &asOf + } + + existing := &model.RateSnapshot{} + filter := repository.Filter("rateRef", snapshot.RateRef) + err := r.repo.FindOneByFilter(ctx, filter, existing) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + r.logger.Debug("inserting new rate snapshot", zap.String("rate_ref", snapshot.RateRef)) + return r.repo.Insert(ctx, snapshot, filter) + } + r.logger.Error("failed to query rate snapshot", zap.Error(err), zap.String("rate_ref", snapshot.RateRef)) + return err + } + + if existing.GetID() != nil { + snapshot.SetID(*existing.GetID()) + } + r.logger.Debug("updating rate snapshot", zap.String("rate_ref", snapshot.RateRef)) + return r.repo.Update(ctx, snapshot) +} + +func (r *ratesStore) LatestSnapshot(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) { + query := repository.Query(). + Filter(repository.Field("pair").Dot("base"), pair.Base). + Filter(repository.Field("pair").Dot("quote"), pair.Quote) + + if provider != "" { + query = query.Filter(repository.Field("provider"), provider) + } + + limit := int64(1) + query = query.Sort(repository.Field("asOfUnixMs"), false).Limit(&limit) + + var result *model.RateSnapshot + err := r.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.RateSnapshot{} + if err := cur.Decode(doc); err != nil { + return err + } + result = doc + return nil + }) + if err != nil { + return nil, err + } + if result == nil { + return nil, merrors.ErrNoData + } + return result, nil +} diff --git a/api/fx/storage/mongo/store/rates_test.go b/api/fx/storage/mongo/store/rates_test.go new file mode 100644 index 0000000..023ff04 --- /dev/null +++ b/api/fx/storage/mongo/store/rates_test.go @@ -0,0 +1,87 @@ +package store + +import ( + "context" + "testing" + "time" + + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestRatesStoreUpsertInsert(t *testing.T) { + ctx := context.Background() + var inserted *model.RateSnapshot + + repo := &repoStub{ + findOneFn: func(context.Context, builder.Query, storable.Storable) error { + return merrors.ErrNoData + }, + insertFn: func(_ context.Context, obj storable.Storable, _ builder.Query) error { + inserted = cloneRate(t, obj) + return nil + }, + } + store := &ratesStore{logger: zap.NewNop(), repo: repo} + + snapshot := &model.RateSnapshot{RateRef: "r1"} + if err := store.UpsertSnapshot(ctx, snapshot); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if inserted == nil || inserted.RateRef != "r1" { + t.Fatalf("expected snapshot to be inserted") + } +} + +func TestRatesStoreUpsertUpdate(t *testing.T) { + ctx := context.Background() + existingID := primitive.NewObjectID() + var updated *model.RateSnapshot + + repo := &repoStub{ + findOneFn: func(_ context.Context, _ builder.Query, result storable.Storable) error { + snap := result.(*model.RateSnapshot) + snap.SetID(existingID) + snap.RateRef = "existing" + return nil + }, + updateFn: func(_ context.Context, obj storable.Storable) error { + snap := obj.(*model.RateSnapshot) + updated = snap + return nil + }, + } + + store := &ratesStore{logger: zap.NewNop(), repo: repo} + toUpdate := &model.RateSnapshot{RateRef: "existing"} + if err := store.UpsertSnapshot(ctx, toUpdate); err != nil { + t.Fatalf("unexpected error: %v", err) + } + if updated == nil || updated.GetID() == nil || *updated.GetID() != existingID { + t.Fatalf("expected update to preserve ID") + } +} + +func TestRatesStoreLatestSnapshot(t *testing.T) { + now := time.Now().UnixMilli() + repo := &repoStub{ + findManyFn: func(_ context.Context, _ builder.Query, decode rd.DecodingFunc) error { + doc := &model.RateSnapshot{RateRef: "latest", AsOfUnixMs: now} + return runDecoderWithDocs(t, decode, doc) + }, + } + + store := &ratesStore{logger: zap.NewNop(), repo: repo} + res, err := store.LatestSnapshot(context.Background(), model.CurrencyPair{Base: "USD", Quote: "EUR"}, "") + if err != nil { + t.Fatalf("unexpected error: %v", err) + } + if res.RateRef != "latest" || res.AsOfUnixMs != now { + t.Fatalf("unexpected snapshot returned: %+v", res) + } +} diff --git a/api/fx/storage/mongo/store/testing_helpers_test.go b/api/fx/storage/mongo/store/testing_helpers_test.go new file mode 100644 index 0000000..261e936 --- /dev/null +++ b/api/fx/storage/mongo/store/testing_helpers_test.go @@ -0,0 +1,189 @@ +package store + +import ( + "context" + "testing" + + "github.com/tech/sendico/fx/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/db/transaction" + "github.com/tech/sendico/pkg/merrors" + pmodel "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type repoStub struct { + insertFn func(ctx context.Context, obj storable.Storable, filter builder.Query) error + insertManyFn func(ctx context.Context, objects []storable.Storable) error + findOneFn func(ctx context.Context, query builder.Query, result storable.Storable) error + findManyFn func(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error + updateFn func(ctx context.Context, obj storable.Storable) error + patchManyFn func(ctx context.Context, filter builder.Query, patch builder.Patch) (int, error) + createIdxFn func(def *ri.Definition) error +} + +func (r *repoStub) Aggregate(ctx context.Context, b builder.Pipeline, decoder rd.DecodingFunc) error { + return merrors.NotImplemented("Aggregate not used") +} + +func (r *repoStub) Insert(ctx context.Context, obj storable.Storable, filter builder.Query) error { + if r.insertFn != nil { + return r.insertFn(ctx, obj, filter) + } + return nil +} + +func (r *repoStub) InsertMany(ctx context.Context, objects []storable.Storable) error { + if r.insertManyFn != nil { + return r.insertManyFn(ctx, objects) + } + return nil +} + +func (r *repoStub) Get(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + return merrors.NotImplemented("Get not used") +} + +func (r *repoStub) FindOneByFilter(ctx context.Context, query builder.Query, result storable.Storable) error { + if r.findOneFn != nil { + return r.findOneFn(ctx, query, result) + } + return nil +} + +func (r *repoStub) FindManyByFilter(ctx context.Context, query builder.Query, decoder rd.DecodingFunc) error { + if r.findManyFn != nil { + return r.findManyFn(ctx, query, decoder) + } + return nil +} + +func (r *repoStub) Update(ctx context.Context, obj storable.Storable) error { + if r.updateFn != nil { + return r.updateFn(ctx, obj) + } + return nil +} + +func (r *repoStub) Patch(ctx context.Context, id primitive.ObjectID, patch builder.Patch) error { + return merrors.NotImplemented("Patch not used") +} + +func (r *repoStub) PatchMany(ctx context.Context, filter builder.Query, patch builder.Patch) (int, error) { + if r.patchManyFn != nil { + return r.patchManyFn(ctx, filter, patch) + } + return 0, nil +} + +func (r *repoStub) Delete(ctx context.Context, id primitive.ObjectID) error { + return merrors.NotImplemented("Delete not used") +} + +func (r *repoStub) DeleteMany(ctx context.Context, query builder.Query) error { + return merrors.NotImplemented("DeleteMany not used") +} + +func (r *repoStub) CreateIndex(def *ri.Definition) error { + if r.createIdxFn != nil { + return r.createIdxFn(def) + } + return nil +} + +func (r *repoStub) ListIDs(ctx context.Context, query builder.Query) ([]primitive.ObjectID, error) { + return nil, merrors.NotImplemented("ListIDs not used") +} + +func (r *repoStub) ListPermissionBound(ctx context.Context, query builder.Query) ([]pmodel.PermissionBoundStorable, error) { + return nil, merrors.NotImplemented("ListPermissionBound not used") +} + +func (r *repoStub) ListAccountBound(ctx context.Context, query builder.Query) ([]pmodel.AccountBoundStorable, error) { + return nil, merrors.NotImplemented("ListAccountBound not used") +} + +func (r *repoStub) Collection() string { return "test" } + +type txFactoryStub struct { + executeFn func(ctx context.Context, cb transaction.Callback) (any, error) +} + +func (f *txFactoryStub) CreateTransaction() transaction.Transaction { + return &txStub{executeFn: f.executeFn} +} + +type txStub struct { + executeFn func(ctx context.Context, cb transaction.Callback) (any, error) +} + +func (t *txStub) Execute(ctx context.Context, cb transaction.Callback) (any, error) { + if t.executeFn != nil { + return t.executeFn(ctx, cb) + } + return cb(ctx) +} + +func cloneRate(t *testing.T, obj storable.Storable) *model.RateSnapshot { + t.Helper() + rate, ok := obj.(*model.RateSnapshot) + if !ok { + t.Fatalf("expected *model.RateSnapshot, got %T", obj) + } + copy := *rate + return © +} + +func cloneQuote(t *testing.T, obj storable.Storable) *model.Quote { + t.Helper() + quote, ok := obj.(*model.Quote) + if !ok { + t.Fatalf("expected *model.Quote, got %T", obj) + } + copy := *quote + return © +} + +func clonePair(t *testing.T, obj storable.Storable) *model.Pair { + t.Helper() + pair, ok := obj.(*model.Pair) + if !ok { + t.Fatalf("expected *model.Pair, got %T", obj) + } + copy := *pair + return © +} + +func cloneCurrency(t *testing.T, obj storable.Storable) *model.Currency { + t.Helper() + currency, ok := obj.(*model.Currency) + if !ok { + t.Fatalf("expected *model.Currency, got %T", obj) + } + copy := *currency + return © +} + +func runDecoderWithDocs(t *testing.T, decode rd.DecodingFunc, docs ...interface{}) error { + t.Helper() + cur, err := mongo.NewCursorFromDocuments(docs, nil, nil) + if err != nil { + t.Fatalf("failed to create cursor: %v", err) + } + defer cur.Close(context.Background()) + + if len(docs) > 0 { + if !cur.Next(context.Background()) { + return cur.Err() + } + } + + if err := decode(cur); err != nil { + return err + } + return cur.Err() +} diff --git a/api/fx/storage/mongo/transaction.go b/api/fx/storage/mongo/transaction.go new file mode 100644 index 0000000..64b7b65 --- /dev/null +++ b/api/fx/storage/mongo/transaction.go @@ -0,0 +1,38 @@ +package mongo + +import ( + "context" + + "github.com/tech/sendico/pkg/db/transaction" + "go.mongodb.org/mongo-driver/mongo" +) + +type mongoTransactionFactory struct { + client *mongo.Client +} + +func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction { + return &mongoTransaction{client: f.client} +} + +type mongoTransaction struct { + client *mongo.Client +} + +func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) { + session, err := t.client.StartSession() + if err != nil { + return nil, err + } + defer session.EndSession(ctx) + + run := func(sessCtx mongo.SessionContext) (any, error) { + return cb(sessCtx) + } + + return session.WithTransaction(ctx, run) +} + +func newMongoTransactionFactory(client *mongo.Client) transaction.Factory { + return &mongoTransactionFactory{client: client} +} diff --git a/api/fx/storage/storage.go b/api/fx/storage/storage.go new file mode 100644 index 0000000..691f378 --- /dev/null +++ b/api/fx/storage/storage.go @@ -0,0 +1,53 @@ +package storage + +import ( + "context" + "time" + + "github.com/tech/sendico/fx/storage/model" +) + +type storageError string + +func (e storageError) Error() string { + return string(e) +} + +var ( + ErrQuoteExpired = storageError("fx.storage: quote expired") + ErrQuoteConsumed = storageError("fx.storage: quote consumed") + ErrQuoteNotFirm = storageError("fx.storage: quote is not firm") + ErrQuoteConsumptionRace = storageError("fx.storage: quote consumption collision") +) + +type Repository interface { + Ping(ctx context.Context) error + Rates() RatesStore + Quotes() QuotesStore + Pairs() PairStore + Currencies() CurrencyStore +} + +type RatesStore interface { + UpsertSnapshot(ctx context.Context, snapshot *model.RateSnapshot) error + LatestSnapshot(ctx context.Context, pair model.CurrencyPair, provider string) (*model.RateSnapshot, error) +} + +type QuotesStore interface { + Issue(ctx context.Context, quote *model.Quote) error + GetByRef(ctx context.Context, quoteRef string) (*model.Quote, error) + Consume(ctx context.Context, quoteRef, ledgerTxnRef string, when time.Time) (*model.Quote, error) + ExpireIssuedBefore(ctx context.Context, cutoff time.Time) (int, error) +} + +type PairStore interface { + ListEnabled(ctx context.Context) ([]*model.Pair, error) + Get(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) + Upsert(ctx context.Context, p *model.Pair) error +} + +type CurrencyStore interface { + Get(ctx context.Context, code string) (*model.Currency, error) + List(ctx context.Context, codes ...string) ([]*model.Currency, error) + Upsert(ctx context.Context, currency *model.Currency) error +} diff --git a/api/ledger/.air.toml b/api/ledger/.air.toml new file mode 100644 index 0000000..bfc83bc --- /dev/null +++ b/api/ledger/.air.toml @@ -0,0 +1,32 @@ +# Config file for Air in TOML format + +root = "./../.." +tmp_dir = "tmp" + +[build] +cmd = "go build -o app -ldflags \"-X 'github.com/tech/sendico/ledger/internal/appversion.BuildUser=$(whoami)' -X 'github.com/tech/sendico/ledger/internal/appversion.Version=$APP_V' -X 'github.com/tech/sendico/ledger/internal/appversion.Branch=$BUILD_BRANCH' -X 'github.com/tech/sendico/ledger/internal/appversion.Revision=$GIT_REV' -X 'github.com/tech/sendico/ledger/internal/appversion.BuildDate=$(date)'\"" +bin = "./app" +full_bin = "./app --debug --config.file=config.yml" +include_ext = ["go", "yaml", "yml"] +exclude_dir = ["ledger/tmp", "pkg/.git", "ledger/env"] +exclude_regex = ["_test\\.go"] +exclude_unchanged = true +follow_symlink = true +log = "air.log" +delay = 0 +stop_on_error = true +send_interrupt = true +kill_delay = 500 +args_bin = [] + +[log] +time = false + +[color] +main = "magenta" +watcher = "cyan" +build = "yellow" +runner = "green" + +[misc] +clean_on_exit = true diff --git a/api/ledger/.gitignore b/api/ledger/.gitignore new file mode 100644 index 0000000..dc67a7e --- /dev/null +++ b/api/ledger/.gitignore @@ -0,0 +1,3 @@ +internal/generated +.gocache +app \ No newline at end of file diff --git a/api/ledger/METRICS.md b/api/ledger/METRICS.md new file mode 100644 index 0000000..26eaea9 --- /dev/null +++ b/api/ledger/METRICS.md @@ -0,0 +1,306 @@ +# Ledger Service - Prometheus Metrics + +## Overview + +The Ledger service exposes Prometheus metrics on the metrics endpoint (default: `:9401/metrics`). This provides operational visibility into ledger operations, performance, and errors. + +## Metrics Endpoint + +- **URL**: `http://localhost:9401/metrics` +- **Format**: Prometheus exposition format +- **Configuration**: Set via `config.yml` → `metrics.address` + +## Available Metrics + +### 1. Journal Entry Operations + +#### `ledger_journal_entries_total` +**Type**: Counter +**Description**: Total number of journal entries posted to the ledger +**Labels**: +- `entry_type`: Type of journal entry (`credit`, `debit`, `transfer`, `fx`, `fee`, `adjust`, `reverse`) +- `status`: Operation status (`success`, `error`, `attempted`) + +**Example**: +```promql +# Count of successful credit entries +ledger_journal_entries_total{entry_type="credit", status="success"} + +# Rate of failed transfers +rate(ledger_journal_entries_total{entry_type="transfer", status="error"}[5m]) +``` + +--- + +#### `ledger_journal_entry_duration_seconds` +**Type**: Histogram +**Description**: Duration of journal entry posting operations +**Labels**: +- `entry_type`: Type of journal entry + +**Buckets**: `[.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10]` seconds + +**Example**: +```promql +# 95th percentile latency for credit postings +histogram_quantile(0.95, rate(ledger_journal_entry_duration_seconds_bucket{entry_type="credit"}[5m])) + +# Average duration for all entry types +rate(ledger_journal_entry_duration_seconds_sum[5m]) / rate(ledger_journal_entry_duration_seconds_count[5m]) +``` + +--- + +#### `ledger_journal_entry_errors_total` +**Type**: Counter +**Description**: Total number of journal entry posting errors +**Labels**: +- `entry_type`: Type of journal entry +- `error_type`: Error classification (`validation`, `insufficient_funds`, `db_error`, `not_implemented`, etc.) + +**Example**: +```promql +# Errors by type +sum by (error_type) (ledger_journal_entry_errors_total) + +# Validation error rate for transfers +rate(ledger_journal_entry_errors_total{entry_type="transfer", error_type="validation"}[5m]) +``` + +--- + +### 2. Balance Operations + +#### `ledger_balance_queries_total` +**Type**: Counter +**Description**: Total number of balance queries +**Labels**: +- `status`: Query status (`success`, `error`) + +**Example**: +```promql +# Balance query success rate +rate(ledger_balance_queries_total{status="success"}[5m]) / rate(ledger_balance_queries_total[5m]) +``` + +--- + +#### `ledger_balance_query_duration_seconds` +**Type**: Histogram +**Description**: Duration of balance query operations +**Labels**: +- `status`: Query status + +**Example**: +```promql +# 99th percentile balance query latency +histogram_quantile(0.99, rate(ledger_balance_query_duration_seconds_bucket[5m])) +``` + +--- + +### 3. Reversal Operations + +#### `ledger_reversals_total` +**Type**: Counter +**Description**: Total number of journal entry reversals +**Labels**: +- `status`: Reversal status (`success`, `error`) + +**Example**: +```promql +# Reversal error rate +rate(ledger_reversals_total{status="error"}[5m]) +``` + +--- + +### 4. Transaction Amounts + +#### `ledger_transaction_amount` +**Type**: Histogram +**Description**: Distribution of transaction amounts (normalized) +**Labels**: +- `currency`: Currency code (`USD`, `EUR`, `GBP`, etc.) +- `entry_type`: Type of journal entry + +**Buckets**: `[1, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000]` + +**Example**: +```promql +# Average transaction amount in USD for credits +rate(ledger_transaction_amount_sum{currency="USD", entry_type="credit"}[5m]) / +rate(ledger_transaction_amount_count{currency="USD", entry_type="credit"}[5m]) + +# 90th percentile transaction amount +histogram_quantile(0.90, rate(ledger_transaction_amount_bucket[5m])) +``` + +--- + +### 5. Account Operations + +#### `ledger_account_operations_total` +**Type**: Counter +**Description**: Total number of account-level operations +**Labels**: +- `operation`: Operation type (`create`, `freeze`, `unfreeze`) +- `status`: Operation status (`success`, `error`) + +**Example**: +```promql +# Account creation rate +rate(ledger_account_operations_total{operation="create"}[5m]) +``` + +--- + +### 6. Idempotency + +#### `ledger_duplicate_requests_total` +**Type**: Counter +**Description**: Total number of duplicate requests detected via idempotency keys +**Labels**: +- `entry_type`: Type of journal entry + +**Example**: +```promql +# Duplicate request rate (indicates retry behavior) +rate(ledger_duplicate_requests_total[5m]) + +# Percentage of duplicate requests +rate(ledger_duplicate_requests_total[5m]) / rate(ledger_journal_entries_total[5m]) * 100 +``` + +--- + +### 7. gRPC Metrics (Built-in) + +These are automatically provided by the gRPC framework: + +#### `grpc_server_requests_total` +**Type**: Counter +**Labels**: `grpc_service`, `grpc_method`, `grpc_type`, `grpc_code` + +#### `grpc_server_handling_seconds` +**Type**: Histogram +**Labels**: `grpc_service`, `grpc_method`, `grpc_type`, `grpc_code` + +**Example**: +```promql +# gRPC error rate by method +rate(grpc_server_requests_total{grpc_code!="OK"}[5m]) + +# P95 latency for PostCredit RPC +histogram_quantile(0.95, rate(grpc_server_handling_seconds_bucket{grpc_method="PostCreditWithCharges"}[5m])) +``` + +--- + +## Common Queries + +### Health & Availability + +```promql +# Overall request rate +sum(rate(grpc_server_requests_total[5m])) + +# Error rate (all operations) +sum(rate(ledger_journal_entry_errors_total[5m])) + +# Success rate for journal entries +sum(rate(ledger_journal_entries_total{status="success"}[5m])) / sum(rate(ledger_journal_entries_total[5m])) +``` + +### Performance + +```promql +# P99 latency for all journal entry types +histogram_quantile(0.99, sum(rate(ledger_journal_entry_duration_seconds_bucket[5m])) by (le, entry_type)) + +# Slowest operation types +topk(5, avg by (entry_type) (rate(ledger_journal_entry_duration_seconds_sum[5m]) / rate(ledger_journal_entry_duration_seconds_count[5m]))) +``` + +### Business Insights + +```promql +# Transaction volume by type +sum by (entry_type) (rate(ledger_journal_entries_total{status="success"}[1h])) + +# Total money flow (sum of transaction amounts) +sum(rate(ledger_transaction_amount_sum[5m])) + +# Most common error types +topk(10, sum by (error_type) (rate(ledger_journal_entry_errors_total[5m]))) +``` + +--- + +## Grafana Dashboard + +### Recommended Panels + +1. **Request Rate** - `sum(rate(grpc_server_requests_total[5m]))` +2. **Error Rate** - `sum(rate(grpc_server_requests_total{grpc_code!="OK"}[5m]))` +3. **P95/P99 Latency** - Histogram quantiles +4. **Operations by Type** - Stacked graph of `ledger_journal_entries_total` +5. **Error Breakdown** - Pie chart of `ledger_journal_entry_errors_total` by `error_type` +6. **Transaction Volume** - Counter of successful entries +7. **Duplicate Requests** - `ledger_duplicate_requests_total` rate + +--- + +## Alerting Rules + +### Critical + +```yaml +# High error rate +- alert: LedgerHighErrorRate + expr: rate(ledger_journal_entry_errors_total[5m]) > 10 + for: 5m + labels: + severity: critical + +# Service unavailable +- alert: LedgerServiceDown + expr: up{job="ledger"} == 0 + for: 1m + labels: + severity: critical +``` + +### Warning + +```yaml +# Slow operations +- alert: LedgerSlowOperations + expr: histogram_quantile(0.95, rate(ledger_journal_entry_duration_seconds_bucket[5m])) > 1 + for: 10m + labels: + severity: warning + +# High duplicate request rate (potential retry storm) +- alert: LedgerHighDuplicateRate + expr: rate(ledger_duplicate_requests_total[5m]) / rate(ledger_journal_entries_total[5m]) > 0.2 + for: 5m + labels: + severity: warning +``` + +--- + +## Configuration + +Metrics are configured in `config.yml`: + +```yaml +metrics: + address: ":9401" # Metrics HTTP server address +``` + +## Dependencies + +- Prometheus client library: `github.com/prometheus/client_golang` +- All metrics are registered globally and exposed via `/metrics` endpoint diff --git a/api/ledger/client/client.go b/api/ledger/client/client.go new file mode 100644 index 0000000..f71927c --- /dev/null +++ b/api/ledger/client/client.go @@ -0,0 +1,142 @@ +package client + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "strings" + "time" + + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Client exposes typed helpers around the ledger gRPC API. +type Client interface { + PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) + PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) + TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) + ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) + + GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) + GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) + GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) + + Close() error +} + +type grpcLedgerClient interface { + PostCreditWithCharges(ctx context.Context, in *ledgerv1.PostCreditRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) + PostDebitWithCharges(ctx context.Context, in *ledgerv1.PostDebitRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) + TransferInternal(ctx context.Context, in *ledgerv1.TransferRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) + ApplyFXWithCharges(ctx context.Context, in *ledgerv1.FXRequest, opts ...grpc.CallOption) (*ledgerv1.PostResponse, error) + GetBalance(ctx context.Context, in *ledgerv1.GetBalanceRequest, opts ...grpc.CallOption) (*ledgerv1.BalanceResponse, error) + GetJournalEntry(ctx context.Context, in *ledgerv1.GetEntryRequest, opts ...grpc.CallOption) (*ledgerv1.JournalEntryResponse, error) + GetStatement(ctx context.Context, in *ledgerv1.GetStatementRequest, opts ...grpc.CallOption) (*ledgerv1.StatementResponse, error) +} + +type ledgerClient struct { + cfg Config + conn *grpc.ClientConn + client grpcLedgerClient +} + +// New dials the ledger endpoint and returns a ready client. +func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { + cfg.setDefaults() + if strings.TrimSpace(cfg.Address) == "" { + return nil, errors.New("ledger: address is required") + } + + dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) + defer cancel() + + dialOpts := make([]grpc.DialOption, 0, len(opts)+1) + dialOpts = append(dialOpts, opts...) + + if cfg.Insecure { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + } + + conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) + if err != nil { + return nil, fmt.Errorf("ledger: dial %s: %w", cfg.Address, err) + } + + return &ledgerClient{ + cfg: cfg, + conn: conn, + client: ledgerv1.NewLedgerServiceClient(conn), + }, nil +} + +// NewWithClient injects a pre-built ledger client (useful for tests). +func NewWithClient(cfg Config, lc grpcLedgerClient) Client { + cfg.setDefaults() + return &ledgerClient{ + cfg: cfg, + client: lc, + } +} + +func (c *ledgerClient) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +func (c *ledgerClient) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.PostCreditWithCharges(ctx, req) +} + +func (c *ledgerClient) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.PostDebitWithCharges(ctx, req) +} + +func (c *ledgerClient) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.TransferInternal(ctx, req) +} + +func (c *ledgerClient) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ApplyFXWithCharges(ctx, req) +} + +func (c *ledgerClient) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetBalance(ctx, req) +} + +func (c *ledgerClient) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetJournalEntry(ctx, req) +} + +func (c *ledgerClient) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetStatement(ctx, req) +} + +func (c *ledgerClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + return context.WithTimeout(ctx, timeout) +} diff --git a/api/ledger/client/config.go b/api/ledger/client/config.go new file mode 100644 index 0000000..364f19d --- /dev/null +++ b/api/ledger/client/config.go @@ -0,0 +1,20 @@ +package client + +import "time" + +// Config captures connection settings for the ledger gRPC service. +type Config struct { + Address string + DialTimeout time.Duration + CallTimeout time.Duration + Insecure bool +} + +func (c *Config) setDefaults() { + if c.DialTimeout <= 0 { + c.DialTimeout = 5 * time.Second + } + if c.CallTimeout <= 0 { + c.CallTimeout = 3 * time.Second + } +} diff --git a/api/ledger/client/fake.go b/api/ledger/client/fake.go new file mode 100644 index 0000000..94ebb60 --- /dev/null +++ b/api/ledger/client/fake.go @@ -0,0 +1,75 @@ +package client + +import ( + "context" + + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" +) + +// Fake implements Client for tests. +type Fake struct { + PostCreditWithChargesFn func(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) + PostDebitWithChargesFn func(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) + TransferInternalFn func(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) + ApplyFXWithChargesFn func(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) + GetBalanceFn func(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) + GetJournalEntryFn func(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) + GetStatementFn func(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) + CloseFn func() error +} + +func (f *Fake) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) { + if f.PostCreditWithChargesFn != nil { + return f.PostCreditWithChargesFn(ctx, req) + } + return &ledgerv1.PostResponse{}, nil +} + +func (f *Fake) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) { + if f.PostDebitWithChargesFn != nil { + return f.PostDebitWithChargesFn(ctx, req) + } + return &ledgerv1.PostResponse{}, nil +} + +func (f *Fake) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + if f.TransferInternalFn != nil { + return f.TransferInternalFn(ctx, req) + } + return &ledgerv1.PostResponse{}, nil +} + +func (f *Fake) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) { + if f.ApplyFXWithChargesFn != nil { + return f.ApplyFXWithChargesFn(ctx, req) + } + return &ledgerv1.PostResponse{}, nil +} + +func (f *Fake) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) { + if f.GetBalanceFn != nil { + return f.GetBalanceFn(ctx, req) + } + return &ledgerv1.BalanceResponse{}, nil +} + +func (f *Fake) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) { + if f.GetJournalEntryFn != nil { + return f.GetJournalEntryFn(ctx, req) + } + return &ledgerv1.JournalEntryResponse{}, nil +} + +func (f *Fake) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) { + if f.GetStatementFn != nil { + return f.GetStatementFn(ctx, req) + } + return &ledgerv1.StatementResponse{}, nil +} + +func (f *Fake) Close() error { + if f.CloseFn != nil { + return f.CloseFn() + } + return nil +} diff --git a/api/ledger/config.yml b/api/ledger/config.yml new file mode 100644 index 0000000..b4aa05d --- /dev/null +++ b/api/ledger/config.yml @@ -0,0 +1,38 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50052" + enable_reflection: true + enable_health: true + +metrics: + address: ":9401" + +database: + driver: mongodb + settings: + host_env: LEDGER_MONGO_HOST + port_env: LEDGER_MONGO_PORT + database_env: LEDGER_MONGO_DATABASE + user_env: LEDGER_MONGO_USER + password_env: LEDGER_MONGO_PASSWORD + auth_source_env: LEDGER_MONGO_AUTH_SOURCE + replica_set_env: LEDGER_MONGO_REPLICA_SET + +messaging: + driver: NATS + settings: + url_env: NATS_URL + host_env: NATS_HOST + port_env: NATS_PORT + username_env: NATS_USER + password_env: NATS_PASSWORD + broker_name: Ledger Service + max_reconnects: 10 + reconnect_wait: 5 + +fees: + address: "sendico_billing_fees:50060" + timeout_seconds: 3 diff --git a/api/ledger/env/.gitignore b/api/ledger/env/.gitignore new file mode 100644 index 0000000..f2a8cbe --- /dev/null +++ b/api/ledger/env/.gitignore @@ -0,0 +1 @@ +.env.api diff --git a/api/ledger/go.mod b/api/ledger/go.mod new file mode 100644 index 0000000..fe40de8 --- /dev/null +++ b/api/ledger/go.mod @@ -0,0 +1,55 @@ +module github.com/tech/sendico/ledger + +go 1.24.0 + +replace github.com/tech/sendico/pkg => ../pkg + +require ( + github.com/prometheus/client_golang v1.23.2 + github.com/shopspring/decimal v1.4.0 + github.com/stretchr/testify v1.11.1 + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver v1.17.6 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/davecgh/go-spew v1.1.1 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mattn/go-colorable v0.1.14 // indirect + github.com/mattn/go-isatty v0.0.20 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/pmezard/go-difflib v1.0.0 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect +) diff --git a/api/ledger/go.sum b/api/ledger/go.sum new file mode 100644 index 0000000..f059ae9 --- /dev/null +++ b/api/ledger/go.sum @@ -0,0 +1,227 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= +github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +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/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +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/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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc= +github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/ledger/internal/appversion/version.go b/api/ledger/internal/appversion/version.go new file mode 100644 index 0000000..fa7d39d --- /dev/null +++ b/api/ledger/internal/appversion/version.go @@ -0,0 +1,27 @@ +package appversion + +import ( + "github.com/tech/sendico/pkg/version" + vf "github.com/tech/sendico/pkg/version/factory" +) + +// Build information. Populated at build-time. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +func Create() version.Printer { + vi := version.Info{ + Program: "MeetX Connectica Ledger Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&vi) +} diff --git a/api/ledger/internal/model/account.go b/api/ledger/internal/model/account.go new file mode 100644 index 0000000..89015da --- /dev/null +++ b/api/ledger/internal/model/account.go @@ -0,0 +1,133 @@ +package ledger + +import ( + "regexp" + "strconv" + "strings" + "time" + + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AccountType controls normal balance side. +type AccountType string + +const ( + AccountTypeAsset AccountType = "asset" + AccountTypeLiability AccountType = "liability" + AccountTypeRevenue AccountType = "revenue" + AccountTypeExpense AccountType = "expense" +) + +type AccountStatus string + +const ( + AccountStatusActive AccountStatus = "active" + AccountStatusFrozen AccountStatus = "frozen" +) + +// lowercase a-z0-9 segments separated by ':' +var accountKeyRe = regexp.MustCompile(`^[a-z0-9]+(?:[:][a-z0-9]+)*$`) + +type Account struct { + model.PermissionBound `bson:",inline" json:",inline"` + + // Immutable identifier used by postings, balances, etc. + AccountKey string `bson:"accountKey" json:"accountKey"` // e.g., "asset:cash:operating" + PathParts []string `bson:"pathParts,omitempty" json:"pathParts,omitempty"` // optional: ["asset","cash","operating"] + + // Classification + AccountType AccountType `bson:"accountType" json:"accountType"` + Currency string `bson:"currency,omitempty" json:"currency,omitempty"` + + // Managing entity in your platform (not legal owner). + OrganizationRef *primitive.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"` + + // Posting policy & lifecycle + AllowNegative bool `bson:"allowNegative" json:"allowNegative"` + Status AccountStatus `bson:"status" json:"status"` + + // Legal ownership history + Ownerships []Ownership `bson:"ownerships,omitempty" json:"ownerships,omitempty"` + CurrentOwners []Ownership `bson:"currentOwners,omitempty" json:"currentOwners,omitempty"` // denormalized cache + + // Operational flags + IsSettlement bool `bson:"isSettlement,omitempty" json:"isSettlement,omitempty"` +} + +func (a *Account) NormalizeKey() { + a.AccountKey = strings.TrimSpace(strings.ToLower(a.AccountKey)) + if len(a.PathParts) == 0 && a.AccountKey != "" { + a.PathParts = strings.Split(a.AccountKey, ":") + } +} + +func (a *Account) Validate() error { + var verr *ValidationError + + if strings.TrimSpace(a.AccountKey) == "" { + veAdd(&verr, "accountKey", "required", "accountKey is required") + } else if !accountKeyRe.MatchString(a.AccountKey) { + veAdd(&verr, "accountKey", "invalid_format", "use lowercase a-z0-9 segments separated by ':'") + } + + switch a.AccountType { + case AccountTypeAsset, AccountTypeLiability, AccountTypeRevenue, AccountTypeExpense: + default: + veAdd(&verr, "accountType", "invalid", "expected asset|liability|revenue|expense") + } + + switch a.Status { + case AccountStatusActive, AccountStatusFrozen: + default: + veAdd(&verr, "status", "invalid", "expected active|frozen") + } + + // Validate ownership arrays with index context + for i := range a.Ownerships { + if err := a.Ownerships[i].Validate(); err != nil { + veAdd(&verr, "ownerships["+strconv.Itoa(i)+"]", "invalid", err.Error()) + } + } + for i := range a.CurrentOwners { + if err := a.CurrentOwners[i].Validate(); err != nil { + veAdd(&verr, "currentOwners["+strconv.Itoa(i)+"]", "invalid", err.Error()) + } + } + + return verr +} + +// ResolveCurrentOwners recomputes CurrentOwners for a given moment. +func (a *Account) ResolveCurrentOwners(asOf time.Time) { + dst := dstSlice(a.CurrentOwners, 0, len(a.Ownerships)) + for _, o := range a.Ownerships { + if o.ActiveAt(asOf) { + dst = append(dst, o) + } + } + a.CurrentOwners = dst +} + +// BalanceSide returns +1 for debit-normal (asset, expense), -1 for credit-normal (liability, revenue). +func (a *Account) BalanceSide() int { + switch a.AccountType { + case AccountTypeAsset, AccountTypeExpense: + return +1 + default: + return -1 + } +} + +// CloseOwnershipPeriod sets the To date for the first matching active ownership. +func (a *Account) CloseOwnershipPeriod(partyID primitive.ObjectID, role OwnershipRole, to time.Time) bool { + for i := range a.Ownerships { + o := &a.Ownerships[i] + if o.OwnerPartyRef == partyID && o.Role == role && o.ActiveAt(to) { + o.To = &to + return true + } + } + return false +} diff --git a/api/ledger/internal/model/balance.go b/api/ledger/internal/model/balance.go new file mode 100644 index 0000000..12548da --- /dev/null +++ b/api/ledger/internal/model/balance.go @@ -0,0 +1,19 @@ +package ledger + +import ( + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type AccountBalance struct { + model.PermissionBound `bson:",inline" json:",inline"` + LedgerAccountRef primitive.ObjectID `bson:"ledgerAccountRef" json:"ledgerAccountRef"` // unique + Balance decimal.Decimal `bson:"balance" json:"balance"` + Version int64 `bson:"version" json:"version"` // for optimistic locking +} + +func (a *AccountBalance) Collection() string { + return mservice.LedgerBalances +} diff --git a/api/ledger/internal/model/jentry.go b/api/ledger/internal/model/jentry.go new file mode 100644 index 0000000..12c0bea --- /dev/null +++ b/api/ledger/internal/model/jentry.go @@ -0,0 +1,46 @@ +// journal_entry.go +package ledger + +import ( + "time" + + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// EntryType is a closed set of journal entry kinds. +type EntryType string + +const ( + EntryCredit EntryType = "credit" + EntryDebit EntryType = "debit" + EntryTransfer EntryType = "transfer" + EntryFX EntryType = "fx" + EntryFee EntryType = "fee" + EntryAdjust EntryType = "adjust" + EntryReverse EntryType = "reverse" +) + +type JournalEntry struct { + model.PermissionBound `bson:",inline" json:",inline"` + + // Idempotency/de-dup within your chosen scope (e.g., org/request) + IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotencyKey,omitempty"` + EventTime time.Time `bson:"eventTime" json:"eventTime"` + EntryType EntryType `bson:"entryType" json:"entryType"` + Description string `bson:"description,omitempty" json:"description,omitempty"` + + // Monotonic ordering within your chosen scope (e.g., per org/ledger) + Version int64 `bson:"version" json:"version"` + + // Denormalized set of all affected ledger accounts (for entry-level access control & queries) + LedgerAccountRefs []primitive.ObjectID `bson:"ledgerAccountRefs,omitempty" json:"ledgerAccountRefs,omitempty"` + + // Optional backlink for reversals + ReversalOf *primitive.ObjectID `bson:"reversalOf,omitempty" json:"reversalOf,omitempty"` +} + +func (j *JournalEntry) Collection() string { + return mservice.LedgerEntries +} diff --git a/api/ledger/internal/model/outbox.go b/api/ledger/internal/model/outbox.go new file mode 100644 index 0000000..14a945f --- /dev/null +++ b/api/ledger/internal/model/outbox.go @@ -0,0 +1,35 @@ +package ledger + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/mservice" +) + +// Delivery status enum +type OutboxStatus string + +const ( + OutboxPending OutboxStatus = "pending" + OutboxSent OutboxStatus = "sent" + OutboxFailed OutboxStatus = "failed" // terminal after max retries, or keep pending with NextAttemptAt=nil +) + +type OutboxEvent struct { + storable.Base `bson:",inline" json:",inline"` + + EventID string `bson:"eventId" json:"eventId"` // deterministic; use as NATS Msg-Id + Subject string `bson:"subject" json:"subject"` // NATS subject / stream routing key + Payload []byte `bson:"payload" json:"payload"` // JSON (or other) payload + Status OutboxStatus `bson:"status" json:"status"` // enum + Attempts int `bson:"attempts" json:"attempts"` // total tries + NextAttemptAt *time.Time `bson:"nextAttemptAt,omitempty" json:"nextAttemptAt,omitempty"` // for backoff scheduler + SentAt *time.Time `bson:"sentAt,omitempty" json:"sentAt,omitempty"` + LastError string `bson:"lastError,omitempty" json:"lastError,omitempty"` // brief reason of last failure + CorrelationRef string `bson:"correlationRef,omitempty" json:"correlationRef,omitempty"` // e.g., journalEntryRef or idempotencyKey +} + +func (o *OutboxEvent) Collection() string { + return mservice.LedgerOutbox +} diff --git a/api/ledger/internal/model/ownership.go b/api/ledger/internal/model/ownership.go new file mode 100644 index 0000000..d5b9b9b --- /dev/null +++ b/api/ledger/internal/model/ownership.go @@ -0,0 +1,57 @@ +package ledger + +import ( + "time" + + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// OwnershipRole captures legal roles (not permissions). +type OwnershipRole string + +const ( + RoleLegalOwner OwnershipRole = "legal_owner" + RoleBeneficialOwner OwnershipRole = "beneficial_owner" + RoleCustodian OwnershipRole = "custodian" + RoleSignatory OwnershipRole = "signatory" +) + +type Ownership struct { + OwnerPartyRef primitive.ObjectID `bson:"ownerPartyRef" json:"ownerPartyRef"` + Role OwnershipRole `bson:"role" json:"role"` + SharePct *float64 `bson:"sharePct,omitempty" json:"sharePct,omitempty"` // 0..100; nil = unspecified + From time.Time `bson:"effectiveFrom" json:"effectiveFrom"` + To *time.Time `bson:"effectiveTo,omitempty" json:"effectiveTo,omitempty"` // active if t < To; nil = open +} + +func (o *Ownership) Validate() error { + var verr *ValidationError + + if o.OwnerPartyRef.IsZero() { + veAdd(&verr, "ownerPartyRef", "required", "owner party reference required") + } + switch o.Role { + case RoleLegalOwner, RoleBeneficialOwner, RoleCustodian, RoleSignatory: + default: + veAdd(&verr, "role", "invalid", "unknown ownership role") + } + if o.SharePct != nil { + if *o.SharePct < 0 || *o.SharePct > 100 { + veAdd(&verr, "sharePct", "out_of_range", "must be between 0 and 100") + } + } + if o.To != nil && o.To.Before(o.From) { + veAdd(&verr, "effectiveTo", "before_from", "must be >= effectiveFrom") + } + return verr +} + +func (o *Ownership) ActiveAt(t time.Time) bool { + if t.Before(o.From) { + return false + } + if o.To != nil && !t.Before(*o.To) { // active iff t < To + return false + } + return true +} diff --git a/api/ledger/internal/model/party.go b/api/ledger/internal/model/party.go new file mode 100644 index 0000000..10f54a7 --- /dev/null +++ b/api/ledger/internal/model/party.go @@ -0,0 +1,76 @@ +package ledger + +import ( + "encoding/json" + "strings" + + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PartyKind (string-backed enum) — readable in BSON/JSON, safe in Go. +type PartyKind string + +const ( + PartyKindPerson PartyKind = "person" + PartyKindOrganization PartyKind = "organization" + PartyKindExternal PartyKind = "external" // not mapped to internal user/org +) + +func (k *PartyKind) UnmarshalJSON(b []byte) error { + var s string + if err := json.Unmarshal(b, &s); err != nil { + return err + } + switch PartyKind(s) { + case PartyKindPerson, PartyKindOrganization, PartyKindExternal: + *k = PartyKind(s) + return nil + default: + return &ValidationError{Issues: []ValidationIssue{{ + Field: "kind", Code: "invalid_kind", Msg: "expected person|organization|external", + }}} + } +} + +// Party represents a legal person or organization that can own accounts. +// Composed with your storable.Base and model.PermissionBound. +type Party struct { + model.PermissionBound `bson:",inline" json:",inline"` + + Kind PartyKind `bson:"kind" json:"kind"` + Name string `bson:"name" json:"name"` + UserRef *primitive.ObjectID `bson:"userRef,omitempty" json:"userRef,omitempty"` // internal user, if applicable + OrganizationRef *primitive.ObjectID `bson:"organizationRef,omitempty" json:"organizationRef,omitempty"` // internal org, if applicable + // add your own fields here if needed (KYC flags, etc.) +} + +func (p *Party) Collection() string { + return mservice.LedgerParties +} + +func (p *Party) Validate() error { + var verr *ValidationError + + if strings.TrimSpace(p.Name) == "" { + veAdd(&verr, "name", "required", "party name is required") + } + switch p.Kind { + case PartyKindPerson: + if p.OrganizationRef != nil { + veAdd(&verr, "organizationRef", "must_be_nil", "person party cannot have organizationRef") + } + case PartyKindOrganization: + if p.UserRef != nil { + veAdd(&verr, "userRef", "must_be_nil", "organization party cannot have userRef") + } + case PartyKindExternal: + if p.UserRef != nil || p.OrganizationRef != nil { + veAdd(&verr, "refs", "must_be_nil", "external party cannot reference internal user/org") + } + default: + veAdd(&verr, "kind", "invalid", "unknown party kind") + } + return verr +} diff --git a/api/ledger/internal/model/pline.go b/api/ledger/internal/model/pline.go new file mode 100644 index 0000000..419c503 --- /dev/null +++ b/api/ledger/internal/model/pline.go @@ -0,0 +1,37 @@ +// posting_line.go +package ledger + +import ( + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// LineType is a closed set of posting line roles within an entry. +type LineType string + +const ( + LineMain LineType = "main" + LineFee LineType = "fee" + LineSpread LineType = "spread" + LineReversal LineType = "reversal" +) + +type PostingLine struct { + storable.Base `bson:",inline" json:",inline"` + + JournalEntryRef primitive.ObjectID `bson:"journalEntryRef" json:"journalEntryRef"` + LedgerAccountRef primitive.ObjectID `bson:"ledgerAccountRef" json:"ledgerAccountRef"` + + // Amount sign convention: positive = credit, negative = debit + Amount decimal.Decimal `bson:"amount" json:"amount"` + Currency model.Currency `bson:"currency" json:"currency"` + + LineType LineType `bson:"lineType" json:"lineType"` +} + +func (p *PostingLine) Collection() string { + return mservice.LedgerPlines +} diff --git a/api/ledger/internal/model/util.go b/api/ledger/internal/model/util.go new file mode 100644 index 0000000..eac1ffd --- /dev/null +++ b/api/ledger/internal/model/util.go @@ -0,0 +1,10 @@ +package ledger + +// dstSlice returns dst[:n] if capacity is enough, otherwise a new slice with capHint capacity. +// Avoids fmt/errors; tiny helper for in-place reuse when recomputing CurrentOwners. +func dstSlice[T any](dst []T, n, capHint int) []T { + if cap(dst) >= capHint { + return dst[:n] + } + return make([]T, n, capHint) +} diff --git a/api/ledger/internal/model/validation.go b/api/ledger/internal/model/validation.go new file mode 100644 index 0000000..65b3778 --- /dev/null +++ b/api/ledger/internal/model/validation.go @@ -0,0 +1,31 @@ +package ledger + +// ValidationIssue describes a single validation problem. +type ValidationIssue struct { + Field string `json:"field"` + Code string `json:"code"` + Msg string `json:"msg"` +} + +// ValidationError aggregates issues. Implements error without fmt/errors. +type ValidationError struct { + Issues []ValidationIssue `json:"issues"` +} + +func (e *ValidationError) Error() string { + if e == nil || len(e.Issues) == 0 { + return "" + } + if len(e.Issues) == 1 { + return e.Issues[0].Field + ": " + e.Issues[0].Msg + } + return "validation failed" +} + +// veAdd appends a new issue into a (possibly nil) *ValidationError. +func veAdd(e **ValidationError, field, code, msg string) { + if *e == nil { + *e = &ValidationError{Issues: make([]ValidationIssue, 0, 4)} + } + (*e).Issues = append((*e).Issues, ValidationIssue{Field: field, Code: code, Msg: msg}) +} diff --git a/api/ledger/internal/server/internal/serverimp.go b/api/ledger/internal/server/internal/serverimp.go new file mode 100644 index 0000000..2675cf3 --- /dev/null +++ b/api/ledger/internal/server/internal/serverimp.go @@ -0,0 +1,160 @@ +package serverimp + +import ( + "context" + "os" + "strings" + "time" + + "github.com/tech/sendico/ledger/internal/service/ledger" + "github.com/tech/sendico/ledger/storage" + mongostorage "github.com/tech/sendico/ledger/storage/mongo" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/db" + msg "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials/insecure" + "gopkg.in/yaml.v3" +) + +type Imp struct { + logger mlogger.Logger + file string + debug bool + + config *config + app *grpcapp.App[storage.Repository] + service *ledger.Service + feesConn *grpc.ClientConn +} + +type config struct { + *grpcapp.Config `yaml:",inline"` + Fees FeesClientConfig `yaml:"fees"` +} + +type FeesClientConfig struct { + Address string `yaml:"address"` + TimeoutSeconds int `yaml:"timeout_seconds"` +} + +const defaultFeesTimeout = 3 * time.Second + +func (c FeesClientConfig) timeout() time.Duration { + if c.TimeoutSeconds <= 0 { + return defaultFeesTimeout + } + return time.Duration(c.TimeoutSeconds) * time.Second +} + +func Create(logger mlogger.Logger, file string, debug bool) (*Imp, error) { + return &Imp{ + logger: logger.Named("server"), + file: file, + debug: debug, + }, nil +} + +func (i *Imp) Shutdown() { + if i.app == nil { + if i.service != nil { + i.service.Shutdown() + } + if i.feesConn != nil { + _ = i.feesConn.Close() + } + return + } + + timeout := 15 * time.Second + if i.config != nil && i.config.Runtime != nil { + timeout = i.config.Runtime.ShutdownTimeout() + } + + if i.service != nil { + i.service.Shutdown() + } + + ctx, cancel := context.WithTimeout(context.Background(), timeout) + i.app.Shutdown(ctx) + cancel() + + if i.feesConn != nil { + _ = i.feesConn.Close() + } +} + +func (i *Imp) Start() error { + cfg, err := i.loadConfig() + if err != nil { + return err + } + i.config = cfg + + repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { + return mongostorage.New(logger, conn) + } + + var feesClient feesv1.FeeEngineClient + feesTimeout := cfg.Fees.timeout() + if addr := strings.TrimSpace(cfg.Fees.Address); addr != "" { + ctx, cancel := context.WithTimeout(context.Background(), feesTimeout) + defer cancel() + + conn, err := grpc.DialContext(ctx, addr, grpc.WithTransportCredentials(insecure.NewCredentials())) + if err != nil { + i.logger.Warn("Failed to connect to fees service", zap.String("address", addr), zap.Error(err)) + } else { + i.logger.Info("Connected to fees service", zap.String("address", addr)) + i.feesConn = conn + feesClient = feesv1.NewFeeEngineClient(conn) + } + } + + serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { + svc := ledger.NewService(logger, repo, producer, feesClient, feesTimeout) + i.service = svc + return svc, nil + } + + app, err := grpcapp.NewApp(i.logger, "ledger", cfg.Config, i.debug, repoFactory, serviceFactory) + if err != nil { + return err + } + i.app = app + + return i.app.Start() +} + +func (i *Imp) loadConfig() (*config, error) { + data, err := os.ReadFile(i.file) + if err != nil { + i.logger.Error("Could not read configuration file", zap.String("config_file", i.file), zap.Error(err)) + return nil, err + } + + cfg := &config{Config: &grpcapp.Config{}} + if err := yaml.Unmarshal(data, cfg); err != nil { + i.logger.Error("Failed to parse configuration", zap.Error(err)) + return nil, err + } + + if cfg.Runtime == nil { + cfg.Runtime = &grpcapp.RuntimeConfig{ShutdownTimeoutSeconds: 15} + } + + if cfg.GRPC == nil { + cfg.GRPC = &routers.GRPCConfig{ + Network: "tcp", + Address: ":50052", + EnableReflection: true, + EnableHealth: true, + } + } + + return cfg, nil +} diff --git a/api/ledger/internal/server/server.go b/api/ledger/internal/server/server.go new file mode 100644 index 0000000..dc7348e --- /dev/null +++ b/api/ledger/internal/server/server.go @@ -0,0 +1,11 @@ +package server + +import ( + serverimp "github.com/tech/sendico/ledger/internal/server/internal" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" +) + +func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return serverimp.Create(logger, file, debug) +} diff --git a/api/ledger/internal/service/ledger/accounts.go b/api/ledger/internal/service/ledger/accounts.go new file mode 100644 index 0000000..b2343c8 --- /dev/null +++ b/api/ledger/internal/service/ledger/accounts.go @@ -0,0 +1,208 @@ +package ledger + +import ( + "context" + "errors" + "strings" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *Service) createAccountResponder(_ context.Context, req *ledgerv1.CreateAccountRequest) gsresponse.Responder[ledgerv1.CreateAccountResponse] { + return func(ctx context.Context) (*ledgerv1.CreateAccountResponse, error) { + if s.storage == nil { + return nil, errStorageNotInitialized + } + if req == nil { + return nil, merrors.InvalidArgument("request is required") + } + + orgRefStr := strings.TrimSpace(req.GetOrganizationRef()) + if orgRefStr == "" { + return nil, merrors.InvalidArgument("organization_ref is required") + } + orgRef, err := parseObjectID(orgRefStr) + if err != nil { + return nil, err + } + + accountCode := strings.TrimSpace(req.GetAccountCode()) + if accountCode == "" { + return nil, merrors.InvalidArgument("account_code is required") + } + accountCode = strings.ToLower(accountCode) + + currency := strings.TrimSpace(req.GetCurrency()) + if currency == "" { + return nil, merrors.InvalidArgument("currency is required") + } + currency = strings.ToUpper(currency) + + modelType, err := protoAccountTypeToModel(req.GetAccountType()) + if err != nil { + return nil, err + } + + status := req.GetStatus() + if status == ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED { + status = ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE + } + modelStatus, err := protoAccountStatusToModel(status) + if err != nil { + return nil, err + } + + metadata := req.GetMetadata() + if len(metadata) == 0 { + metadata = nil + } + + account := &model.Account{ + AccountCode: accountCode, + Currency: currency, + AccountType: modelType, + Status: modelStatus, + AllowNegative: req.GetAllowNegative(), + IsSettlement: req.GetIsSettlement(), + Metadata: metadata, + } + account.OrganizationRef = orgRef + + err = s.storage.Accounts().Create(ctx, account) + if err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + existing, lookupErr := s.storage.Accounts().GetByAccountCode(ctx, orgRef, accountCode, currency) + if lookupErr != nil { + s.logger.Warn("duplicate account create but failed to load existing", + zap.Error(lookupErr), + zap.String("organizationRef", orgRef.Hex()), + zap.String("accountCode", accountCode), + zap.String("currency", currency)) + return nil, merrors.Internal("failed to load existing account after conflict") + } + recordAccountOperation("create", "duplicate") + return &ledgerv1.CreateAccountResponse{ + Account: toProtoAccount(existing), + }, nil + } + recordAccountOperation("create", "error") + s.logger.Warn("failed to create account", + zap.Error(err), + zap.String("organizationRef", orgRef.Hex()), + zap.String("accountCode", accountCode), + zap.String("currency", currency)) + return nil, merrors.Internal("failed to create account") + } + + recordAccountOperation("create", "success") + return &ledgerv1.CreateAccountResponse{ + Account: toProtoAccount(account), + }, nil + } +} + +func protoAccountTypeToModel(t ledgerv1.AccountType) (model.AccountType, error) { + switch t { + case ledgerv1.AccountType_ACCOUNT_TYPE_ASSET: + return model.AccountTypeAsset, nil + case ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY: + return model.AccountTypeLiability, nil + case ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE: + return model.AccountTypeRevenue, nil + case ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE: + return model.AccountTypeExpense, nil + case ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED: + return "", merrors.InvalidArgument("account_type is required") + default: + return "", merrors.InvalidArgument("invalid account_type") + } +} + +func modelAccountTypeToProto(t model.AccountType) ledgerv1.AccountType { + switch t { + case model.AccountTypeAsset: + return ledgerv1.AccountType_ACCOUNT_TYPE_ASSET + case model.AccountTypeLiability: + return ledgerv1.AccountType_ACCOUNT_TYPE_LIABILITY + case model.AccountTypeRevenue: + return ledgerv1.AccountType_ACCOUNT_TYPE_REVENUE + case model.AccountTypeExpense: + return ledgerv1.AccountType_ACCOUNT_TYPE_EXPENSE + default: + return ledgerv1.AccountType_ACCOUNT_TYPE_UNSPECIFIED + } +} + +func protoAccountStatusToModel(s ledgerv1.AccountStatus) (model.AccountStatus, error) { + switch s { + case ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE: + return model.AccountStatusActive, nil + case ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN: + return model.AccountStatusFrozen, nil + case ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED: + return "", merrors.InvalidArgument("account status is required") + default: + return "", merrors.InvalidArgument("invalid account status") + } +} + +func modelAccountStatusToProto(s model.AccountStatus) ledgerv1.AccountStatus { + switch s { + case model.AccountStatusActive: + return ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE + case model.AccountStatusFrozen: + return ledgerv1.AccountStatus_ACCOUNT_STATUS_FROZEN + default: + return ledgerv1.AccountStatus_ACCOUNT_STATUS_UNSPECIFIED + } +} + +func toProtoAccount(account *model.Account) *ledgerv1.LedgerAccount { + if account == nil { + return nil + } + + var accountRef string + if id := account.GetID(); id != nil && !id.IsZero() { + accountRef = id.Hex() + } + + var organizationRef string + if !account.OrganizationRef.IsZero() { + organizationRef = account.OrganizationRef.Hex() + } + + var createdAt *timestamppb.Timestamp + if !account.CreatedAt.IsZero() { + createdAt = timestamppb.New(account.CreatedAt) + } + + var updatedAt *timestamppb.Timestamp + if !account.UpdatedAt.IsZero() { + updatedAt = timestamppb.New(account.UpdatedAt) + } + + metadata := account.Metadata + if len(metadata) == 0 { + metadata = nil + } + + return &ledgerv1.LedgerAccount{ + LedgerAccountRef: accountRef, + OrganizationRef: organizationRef, + AccountCode: account.AccountCode, + AccountType: modelAccountTypeToProto(account.AccountType), + Currency: account.Currency, + Status: modelAccountStatusToProto(account.Status), + AllowNegative: account.AllowNegative, + IsSettlement: account.IsSettlement, + Metadata: metadata, + CreatedAt: createdAt, + UpdatedAt: updatedAt, + } +} diff --git a/api/ledger/internal/service/ledger/accounts_test.go b/api/ledger/internal/service/ledger/accounts_test.go new file mode 100644 index 0000000..e903917 --- /dev/null +++ b/api/ledger/internal/service/ledger/accounts_test.go @@ -0,0 +1,168 @@ +package ledger + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/merrors" +) + +type accountStoreStub struct { + createErr error + created []*model.Account + existing *model.Account + existingErr error +} + +func (s *accountStoreStub) Create(_ context.Context, account *model.Account) error { + if s.createErr != nil { + return s.createErr + } + if account.GetID() == nil || account.GetID().IsZero() { + account.SetID(primitive.NewObjectID()) + } + account.CreatedAt = account.CreatedAt.UTC() + account.UpdatedAt = account.UpdatedAt.UTC() + s.created = append(s.created, account) + return nil +} + +func (s *accountStoreStub) GetByAccountCode(_ context.Context, _ primitive.ObjectID, _ string, _ string) (*model.Account, error) { + if s.existingErr != nil { + return nil, s.existingErr + } + return s.existing, nil +} + +func (s *accountStoreStub) Get(context.Context, primitive.ObjectID) (*model.Account, error) { + return nil, storage.ErrAccountNotFound +} + +func (s *accountStoreStub) GetDefaultSettlement(context.Context, primitive.ObjectID, string) (*model.Account, error) { + return nil, storage.ErrAccountNotFound +} + +func (s *accountStoreStub) ListByOrganization(context.Context, primitive.ObjectID, int, int) ([]*model.Account, error) { + return nil, nil +} + +func (s *accountStoreStub) UpdateStatus(context.Context, primitive.ObjectID, model.AccountStatus) error { + return nil +} + +type repositoryStub struct { + accounts storage.AccountsStore +} + +func (r *repositoryStub) Ping(context.Context) error { return nil } +func (r *repositoryStub) Accounts() storage.AccountsStore { return r.accounts } +func (r *repositoryStub) JournalEntries() storage.JournalEntriesStore { return nil } +func (r *repositoryStub) PostingLines() storage.PostingLinesStore { return nil } +func (r *repositoryStub) Balances() storage.BalancesStore { return nil } +func (r *repositoryStub) Outbox() storage.OutboxStore { return nil } + +func TestCreateAccountResponder_Success(t *testing.T) { + t.Parallel() + orgRef := primitive.NewObjectID() + + accountStore := &accountStoreStub{} + svc := &Service{ + logger: zap.NewNop(), + storage: &repositoryStub{accounts: accountStore}, + } + + req := &ledgerv1.CreateAccountRequest{ + OrganizationRef: orgRef.Hex(), + AccountCode: "asset:cash:main", + AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, + Currency: "usd", + AllowNegative: false, + IsSettlement: true, + Metadata: map[string]string{"purpose": "primary"}, + } + + resp, err := svc.createAccountResponder(context.Background(), req)(context.Background()) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Account) + + require.Equal(t, "asset:cash:main", resp.Account.AccountCode) + require.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, resp.Account.AccountType) + require.Equal(t, "USD", resp.Account.Currency) + require.True(t, resp.Account.IsSettlement) + require.Contains(t, resp.Account.Metadata, "purpose") + require.NotEmpty(t, resp.Account.LedgerAccountRef) + + require.Len(t, accountStore.created, 1) +} + +func TestCreateAccountResponder_DuplicateReturnsExisting(t *testing.T) { + t.Parallel() + + orgRef := primitive.NewObjectID() + existing := &model.Account{ + AccountCode: "asset:cash:main", + Currency: "USD", + AccountType: model.AccountTypeAsset, + Status: model.AccountStatusActive, + AllowNegative: false, + IsSettlement: true, + Metadata: map[string]string{"purpose": "existing"}, + } + existing.OrganizationRef = orgRef + existing.SetID(primitive.NewObjectID()) + existing.CreatedAt = time.Now().Add(-time.Hour).UTC() + existing.UpdatedAt = time.Now().UTC() + + accountStore := &accountStoreStub{ + createErr: merrors.DataConflict("duplicate"), + existing: existing, + existingErr: nil, + } + + svc := &Service{ + logger: zap.NewNop(), + storage: &repositoryStub{accounts: accountStore}, + } + + req := &ledgerv1.CreateAccountRequest{ + OrganizationRef: orgRef.Hex(), + AccountCode: "asset:cash:main", + AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, + Currency: "usd", + } + + resp, err := svc.createAccountResponder(context.Background(), req)(context.Background()) + require.NoError(t, err) + require.NotNil(t, resp) + require.NotNil(t, resp.Account) + + require.Equal(t, existing.GetID().Hex(), resp.Account.LedgerAccountRef) + require.Equal(t, existing.Metadata["purpose"], resp.Account.Metadata["purpose"]) +} + +func TestCreateAccountResponder_InvalidAccountType(t *testing.T) { + t.Parallel() + + svc := &Service{ + logger: zap.NewNop(), + storage: &repositoryStub{accounts: &accountStoreStub{}}, + } + + req := &ledgerv1.CreateAccountRequest{ + OrganizationRef: primitive.NewObjectID().Hex(), + AccountCode: "asset:cash:main", + Currency: "USD", + } + + _, err := svc.createAccountResponder(context.Background(), req)(context.Background()) + require.Error(t, err) +} diff --git a/api/ledger/internal/service/ledger/helpers.go b/api/ledger/internal/service/ledger/helpers.go new file mode 100644 index 0000000..a5265b3 --- /dev/null +++ b/api/ledger/internal/service/ledger/helpers.go @@ -0,0 +1,166 @@ +package ledger + +import ( + "fmt" + "time" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/merrors" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// parseObjectID converts a hex string to ObjectID +func parseObjectID(hexID string) (primitive.ObjectID, error) { + if hexID == "" { + return primitive.NilObjectID, merrors.InvalidArgument("empty object ID") + } + oid, err := primitive.ObjectIDFromHex(hexID) + if err != nil { + return primitive.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("invalid object ID: %v", err)) + } + return oid, nil +} + +// parseDecimal converts a string amount to decimal +func parseDecimal(amount string) (decimal.Decimal, error) { + if amount == "" { + return decimal.Zero, merrors.InvalidArgument("empty amount") + } + dec, err := decimal.NewFromString(amount) + if err != nil { + return decimal.Zero, merrors.InvalidArgument(fmt.Sprintf("invalid decimal amount: %v", err)) + } + return dec, nil +} + +// validateMoney checks that a Money message is valid +func validateMoney(m *moneyv1.Money, fieldName string) error { + if m == nil { + return merrors.InvalidArgument(fmt.Sprintf("%s: money is required", fieldName)) + } + if m.Amount == "" { + return merrors.InvalidArgument(fmt.Sprintf("%s: amount is required", fieldName)) + } + if m.Currency == "" { + return merrors.InvalidArgument(fmt.Sprintf("%s: currency is required", fieldName)) + } + // Validate it's a valid decimal + if _, err := parseDecimal(m.Amount); err != nil { + return err + } + return nil +} + +// validatePostingLines validates charge lines +func validatePostingLines(lines []*ledgerv1.PostingLine) error { + for i, line := range lines { + if line == nil { + return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: nil posting line", i)) + } + if line.LedgerAccountRef == "" { + return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: ledger_account_ref is required", i)) + } + if line.Money == nil { + return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: money is required", i)) + } + if err := validateMoney(line.Money, fmt.Sprintf("charges[%d].money", i)); err != nil { + return err + } + // Charges should not be MAIN type + if line.LineType == ledgerv1.LineType_LINE_MAIN { + return merrors.InvalidArgument(fmt.Sprintf("charges[%d]: cannot have LINE_MAIN type", i)) + } + } + return nil +} + +// getEventTime extracts event time from proto or defaults to now +func getEventTime(ts *timestamppb.Timestamp) time.Time { + if ts != nil && ts.IsValid() { + return ts.AsTime() + } + return time.Now().UTC() +} + +// protoLineTypeToModel converts proto LineType to model LineType +func protoLineTypeToModel(lt ledgerv1.LineType) model.LineType { + switch lt { + case ledgerv1.LineType_LINE_MAIN: + return model.LineTypeMain + case ledgerv1.LineType_LINE_FEE: + return model.LineTypeFee + case ledgerv1.LineType_LINE_SPREAD: + return model.LineTypeSpread + case ledgerv1.LineType_LINE_REVERSAL: + return model.LineTypeReversal + default: + return model.LineTypeMain + } +} + +// modelLineTypeToProto converts model LineType to proto LineType +func modelLineTypeToProto(lt model.LineType) ledgerv1.LineType { + switch lt { + case model.LineTypeMain: + return ledgerv1.LineType_LINE_MAIN + case model.LineTypeFee: + return ledgerv1.LineType_LINE_FEE + case model.LineTypeSpread: + return ledgerv1.LineType_LINE_SPREAD + case model.LineTypeReversal: + return ledgerv1.LineType_LINE_REVERSAL + default: + return ledgerv1.LineType_LINE_TYPE_UNSPECIFIED + } +} + +// modelEntryTypeToProto converts model EntryType to proto EntryType +func modelEntryTypeToProto(et model.EntryType) ledgerv1.EntryType { + switch et { + case model.EntryTypeCredit: + return ledgerv1.EntryType_ENTRY_CREDIT + case model.EntryTypeDebit: + return ledgerv1.EntryType_ENTRY_DEBIT + case model.EntryTypeTransfer: + return ledgerv1.EntryType_ENTRY_TRANSFER + case model.EntryTypeFX: + return ledgerv1.EntryType_ENTRY_FX + case model.EntryTypeFee: + return ledgerv1.EntryType_ENTRY_FEE + case model.EntryTypeAdjust: + return ledgerv1.EntryType_ENTRY_ADJUST + case model.EntryTypeReverse: + return ledgerv1.EntryType_ENTRY_REVERSE + default: + return ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED + } +} + +// calculateBalance computes net balance from a set of posting lines +func calculateBalance(lines []*model.PostingLine) (decimal.Decimal, error) { + balance := decimal.Zero + for _, line := range lines { + amount, err := parseDecimal(line.Amount) + if err != nil { + return decimal.Zero, fmt.Errorf("invalid line amount: %w", err) + } + balance = balance.Add(amount) + } + return balance, nil +} + +// validateBalanced ensures posting lines sum to zero (double-entry accounting) +func validateBalanced(lines []*model.PostingLine) error { + balance, err := calculateBalance(lines) + if err != nil { + return err + } + if !balance.IsZero() { + return merrors.InvalidArgument(fmt.Sprintf("journal entry must balance (sum=0), got: %s", balance.String())) + } + return nil +} diff --git a/api/ledger/internal/service/ledger/helpers_test.go b/api/ledger/internal/service/ledger/helpers_test.go new file mode 100644 index 0000000..ccb856a --- /dev/null +++ b/api/ledger/internal/service/ledger/helpers_test.go @@ -0,0 +1,417 @@ +package ledger + +import ( + "testing" + "time" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage/model" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + "github.com/shopspring/decimal" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func TestParseObjectID(t *testing.T) { + t.Run("ValidObjectID", func(t *testing.T) { + validID := primitive.NewObjectID() + result, err := parseObjectID(validID.Hex()) + + require.NoError(t, err) + assert.Equal(t, validID, result) + }) + + t.Run("EmptyString", func(t *testing.T) { + result, err := parseObjectID("") + + require.Error(t, err) + assert.Equal(t, primitive.NilObjectID, result) + assert.Contains(t, err.Error(), "empty object ID") + }) + + t.Run("InvalidHexString", func(t *testing.T) { + result, err := parseObjectID("invalid-hex-string") + + require.Error(t, err) + assert.Equal(t, primitive.NilObjectID, result) + assert.Contains(t, err.Error(), "invalid object ID") + }) + + t.Run("IncorrectLength", func(t *testing.T) { + result, err := parseObjectID("abc123") + + require.Error(t, err) + assert.Equal(t, primitive.NilObjectID, result) + }) +} + +func TestParseDecimal(t *testing.T) { + t.Run("ValidDecimal", func(t *testing.T) { + result, err := parseDecimal("123.45") + + require.NoError(t, err) + assert.True(t, result.Equal(decimal.NewFromFloat(123.45))) + }) + + t.Run("EmptyString", func(t *testing.T) { + result, err := parseDecimal("") + + require.Error(t, err) + assert.True(t, result.IsZero()) + assert.Contains(t, err.Error(), "empty amount") + }) + + t.Run("InvalidDecimal", func(t *testing.T) { + result, err := parseDecimal("not-a-number") + + require.Error(t, err) + assert.True(t, result.IsZero()) + assert.Contains(t, err.Error(), "invalid decimal amount") + }) + + t.Run("NegativeDecimal", func(t *testing.T) { + result, err := parseDecimal("-100.50") + + require.NoError(t, err) + assert.True(t, result.Equal(decimal.NewFromFloat(-100.50))) + }) + + t.Run("ZeroDecimal", func(t *testing.T) { + result, err := parseDecimal("0") + + require.NoError(t, err) + assert.True(t, result.IsZero()) + }) +} + +func TestValidateMoney(t *testing.T) { + t.Run("ValidMoney", func(t *testing.T) { + money := &moneyv1.Money{ + Amount: "100.50", + Currency: "USD", + } + + err := validateMoney(money, "test_field") + assert.NoError(t, err) + }) + + t.Run("NilMoney", func(t *testing.T) { + err := validateMoney(nil, "test_field") + + require.Error(t, err) + assert.Contains(t, err.Error(), "test_field: money is required") + }) + + t.Run("EmptyAmount", func(t *testing.T) { + money := &moneyv1.Money{ + Amount: "", + Currency: "USD", + } + + err := validateMoney(money, "test_field") + + require.Error(t, err) + assert.Contains(t, err.Error(), "test_field: amount is required") + }) + + t.Run("EmptyCurrency", func(t *testing.T) { + money := &moneyv1.Money{ + Amount: "100.50", + Currency: "", + } + + err := validateMoney(money, "test_field") + + require.Error(t, err) + assert.Contains(t, err.Error(), "test_field: currency is required") + }) + + t.Run("InvalidAmount", func(t *testing.T) { + money := &moneyv1.Money{ + Amount: "invalid", + Currency: "USD", + } + + err := validateMoney(money, "test_field") + + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid decimal amount") + }) +} + +func TestValidatePostingLines(t *testing.T) { + t.Run("ValidPostingLines", func(t *testing.T) { + lines := []*ledgerv1.PostingLine{ + { + LedgerAccountRef: primitive.NewObjectID().Hex(), + Money: &moneyv1.Money{ + Amount: "10.00", + Currency: "USD", + }, + LineType: ledgerv1.LineType_LINE_FEE, + }, + } + + err := validatePostingLines(lines) + assert.NoError(t, err) + }) + + t.Run("EmptyLines", func(t *testing.T) { + err := validatePostingLines([]*ledgerv1.PostingLine{}) + assert.NoError(t, err) + }) + + t.Run("NilLine", func(t *testing.T) { + lines := []*ledgerv1.PostingLine{nil} + + err := validatePostingLines(lines) + + require.Error(t, err) + assert.Contains(t, err.Error(), "nil posting line") + }) + + t.Run("EmptyAccountRef", func(t *testing.T) { + lines := []*ledgerv1.PostingLine{ + { + LedgerAccountRef: "", + Money: &moneyv1.Money{ + Amount: "10.00", + Currency: "USD", + }, + }, + } + + err := validatePostingLines(lines) + + require.Error(t, err) + assert.Contains(t, err.Error(), "ledger_account_ref is required") + }) + + t.Run("NilMoney", func(t *testing.T) { + lines := []*ledgerv1.PostingLine{ + { + LedgerAccountRef: primitive.NewObjectID().Hex(), + Money: nil, + }, + } + + err := validatePostingLines(lines) + + require.Error(t, err) + assert.Contains(t, err.Error(), "money is required") + }) + + t.Run("MainLineType", func(t *testing.T) { + lines := []*ledgerv1.PostingLine{ + { + LedgerAccountRef: primitive.NewObjectID().Hex(), + Money: &moneyv1.Money{ + Amount: "10.00", + Currency: "USD", + }, + LineType: ledgerv1.LineType_LINE_MAIN, + }, + } + + err := validatePostingLines(lines) + + require.Error(t, err) + assert.Contains(t, err.Error(), "cannot have LINE_MAIN type") + }) +} + +func TestGetEventTime(t *testing.T) { + t.Run("ValidTimestamp", func(t *testing.T) { + now := time.Now() + ts := timestamppb.New(now) + + result := getEventTime(ts) + + assert.True(t, result.Sub(now) < time.Second) + }) + + t.Run("NilTimestamp", func(t *testing.T) { + before := time.Now() + result := getEventTime(nil) + after := time.Now() + + assert.True(t, result.After(before) || result.Equal(before)) + assert.True(t, result.Before(after) || result.Equal(after)) + }) + + t.Run("InvalidTimestamp", func(t *testing.T) { + // Create an invalid timestamp with negative seconds + ts := ×tamppb.Timestamp{Seconds: -1, Nanos: -1} + + // Invalid timestamp should return current time + before := time.Now() + result := getEventTime(ts) + after := time.Now() + + // Result should be close to now since timestamp is invalid + assert.True(t, result.After(before.Add(-time.Second)) || result.Equal(before)) + assert.True(t, result.Before(after.Add(time.Second)) || result.Equal(after)) + }) +} + +func TestProtoLineTypeToModel(t *testing.T) { + tests := []struct { + name string + input ledgerv1.LineType + expected model.LineType + }{ + {"Main", ledgerv1.LineType_LINE_MAIN, model.LineTypeMain}, + {"Fee", ledgerv1.LineType_LINE_FEE, model.LineTypeFee}, + {"Spread", ledgerv1.LineType_LINE_SPREAD, model.LineTypeSpread}, + {"Reversal", ledgerv1.LineType_LINE_REVERSAL, model.LineTypeReversal}, + {"Unspecified", ledgerv1.LineType_LINE_TYPE_UNSPECIFIED, model.LineTypeMain}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := protoLineTypeToModel(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestModelLineTypeToProto(t *testing.T) { + tests := []struct { + name string + input model.LineType + expected ledgerv1.LineType + }{ + {"Main", model.LineTypeMain, ledgerv1.LineType_LINE_MAIN}, + {"Fee", model.LineTypeFee, ledgerv1.LineType_LINE_FEE}, + {"Spread", model.LineTypeSpread, ledgerv1.LineType_LINE_SPREAD}, + {"Reversal", model.LineTypeReversal, ledgerv1.LineType_LINE_REVERSAL}, + {"Unknown", model.LineType("unknown"), ledgerv1.LineType_LINE_TYPE_UNSPECIFIED}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := modelLineTypeToProto(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestModelEntryTypeToProto(t *testing.T) { + tests := []struct { + name string + input model.EntryType + expected ledgerv1.EntryType + }{ + {"Credit", model.EntryTypeCredit, ledgerv1.EntryType_ENTRY_CREDIT}, + {"Debit", model.EntryTypeDebit, ledgerv1.EntryType_ENTRY_DEBIT}, + {"Transfer", model.EntryTypeTransfer, ledgerv1.EntryType_ENTRY_TRANSFER}, + {"FX", model.EntryTypeFX, ledgerv1.EntryType_ENTRY_FX}, + {"Fee", model.EntryTypeFee, ledgerv1.EntryType_ENTRY_FEE}, + {"Adjust", model.EntryTypeAdjust, ledgerv1.EntryType_ENTRY_ADJUST}, + {"Reverse", model.EntryTypeReverse, ledgerv1.EntryType_ENTRY_REVERSE}, + {"Unknown", model.EntryType("unknown"), ledgerv1.EntryType_ENTRY_TYPE_UNSPECIFIED}, + } + + for _, tt := range tests { + t.Run(tt.name, func(t *testing.T) { + result := modelEntryTypeToProto(tt.input) + assert.Equal(t, tt.expected, result) + }) + } +} + +func TestCalculateBalance(t *testing.T) { + t.Run("PositiveBalance", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "100.00"}, + {Amount: "50.00"}, + } + + result, err := calculateBalance(lines) + + require.NoError(t, err) + assert.True(t, result.Equal(decimal.NewFromFloat(150.00))) + }) + + t.Run("NegativeBalance", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "-100.00"}, + {Amount: "-50.00"}, + } + + result, err := calculateBalance(lines) + + require.NoError(t, err) + assert.True(t, result.Equal(decimal.NewFromFloat(-150.00))) + }) + + t.Run("ZeroBalance", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "100.00"}, + {Amount: "-100.00"}, + } + + result, err := calculateBalance(lines) + + require.NoError(t, err) + assert.True(t, result.IsZero()) + }) + + t.Run("EmptyLines", func(t *testing.T) { + result, err := calculateBalance([]*model.PostingLine{}) + + require.NoError(t, err) + assert.True(t, result.IsZero()) + }) + + t.Run("InvalidAmount", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "invalid"}, + } + + _, err := calculateBalance(lines) + require.Error(t, err) + }) +} + +func TestValidateBalanced(t *testing.T) { + t.Run("BalancedEntry", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "100.00"}, // credit + {Amount: "-100.00"}, // debit + } + + err := validateBalanced(lines) + assert.NoError(t, err) + }) + + t.Run("BalancedWithMultipleLines", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "100.00"}, // credit + {Amount: "-50.00"}, // debit + {Amount: "-50.00"}, // debit + } + + err := validateBalanced(lines) + assert.NoError(t, err) + }) + + t.Run("UnbalancedEntry", func(t *testing.T) { + lines := []*model.PostingLine{ + {Amount: "100.00"}, + {Amount: "-50.00"}, + } + + err := validateBalanced(lines) + + require.Error(t, err) + assert.Contains(t, err.Error(), "must balance") + }) + + t.Run("EmptyLines", func(t *testing.T) { + err := validateBalanced([]*model.PostingLine{}) + assert.NoError(t, err) + }) +} diff --git a/api/ledger/internal/service/ledger/metrics.go b/api/ledger/internal/service/ledger/metrics.go new file mode 100644 index 0000000..ad51bab --- /dev/null +++ b/api/ledger/internal/service/ledger/metrics.go @@ -0,0 +1,144 @@ +package ledger + +import ( + "sync" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" +) + +var ( + metricsOnce sync.Once + + // Journal entry operations + journalEntriesTotal *prometheus.CounterVec + journalEntryLatency *prometheus.HistogramVec + journalEntryErrors *prometheus.CounterVec + + // Balance operations + balanceQueriesTotal *prometheus.CounterVec + balanceQueryLatency *prometheus.HistogramVec + + // Transaction amounts + transactionAmounts *prometheus.HistogramVec + + // Account operations + accountOperationsTotal *prometheus.CounterVec + + // Idempotency + duplicateRequestsTotal *prometheus.CounterVec +) + +func initMetrics() { + metricsOnce.Do(func() { + // Journal entries posted by type + journalEntriesTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "ledger_journal_entries_total", + Help: "Total number of journal entries posted to the ledger", + }, + []string{"entry_type", "status"}, // entry_type: credit, debit, transfer, fx, fee, adjust, reverse + ) + + // Journal entry processing latency + journalEntryLatency = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "ledger_journal_entry_duration_seconds", + Help: "Duration of journal entry posting operations", + Buckets: []float64{.001, .005, .01, .025, .05, .1, .25, .5, 1, 2.5, 5, 10}, + }, + []string{"entry_type"}, + ) + + // Journal entry errors by type + journalEntryErrors = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "ledger_journal_entry_errors_total", + Help: "Total number of journal entry posting errors", + }, + []string{"entry_type", "error_type"}, // error_type: validation, insufficient_funds, db_error, etc. + ) + + // Balance queries + balanceQueriesTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "ledger_balance_queries_total", + Help: "Total number of balance queries", + }, + []string{"status"}, // success, error + ) + + // Balance query latency + balanceQueryLatency = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "ledger_balance_query_duration_seconds", + Help: "Duration of balance query operations", + Buckets: prometheus.DefBuckets, + }, + []string{"status"}, + ) + + // Transaction amounts (in normalized form) + transactionAmounts = promauto.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "ledger_transaction_amount", + Help: "Distribution of transaction amounts", + Buckets: []float64{1, 10, 50, 100, 500, 1000, 5000, 10000, 50000, 100000}, + }, + []string{"currency", "entry_type"}, + ) + + // Account operations + accountOperationsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "ledger_account_operations_total", + Help: "Total number of account-level operations", + }, + []string{"operation", "status"}, // operation: create, freeze, unfreeze + ) + + // Duplicate/idempotent requests + duplicateRequestsTotal = promauto.NewCounterVec( + prometheus.CounterOpts{ + Name: "ledger_duplicate_requests_total", + Help: "Total number of duplicate requests detected via idempotency keys", + }, + []string{"entry_type"}, + ) + }) +} + +// Metric recording helpers + +func recordJournalEntry(entryType, status string, durationSeconds float64) { + initMetrics() + journalEntriesTotal.WithLabelValues(entryType, status).Inc() + journalEntryLatency.WithLabelValues(entryType).Observe(durationSeconds) +} + +func recordJournalEntryError(entryType, errorType string) { + initMetrics() + journalEntryErrors.WithLabelValues(entryType, errorType).Inc() + journalEntriesTotal.WithLabelValues(entryType, "error").Inc() +} + +func recordBalanceQuery(status string, durationSeconds float64) { + initMetrics() + balanceQueriesTotal.WithLabelValues(status).Inc() + balanceQueryLatency.WithLabelValues(status).Observe(durationSeconds) +} + +func recordTransactionAmount(currency, entryType string, amount float64) { + initMetrics() + transactionAmounts.WithLabelValues(currency, entryType).Observe(amount) +} + +func recordAccountOperation(operation, status string) { + initMetrics() + accountOperationsTotal.WithLabelValues(operation, status).Inc() +} + +func recordDuplicateRequest(entryType string) { + initMetrics() + duplicateRequestsTotal.WithLabelValues(entryType).Inc() +} diff --git a/api/ledger/internal/service/ledger/outbox_publisher.go b/api/ledger/internal/service/ledger/outbox_publisher.go new file mode 100644 index 0000000..30a3a3d --- /dev/null +++ b/api/ledger/internal/service/ledger/outbox_publisher.go @@ -0,0 +1,206 @@ +package ledger + +import ( + "context" + "encoding/json" + "errors" + "time" + + "github.com/tech/sendico/ledger/storage" + ledgerModel "github.com/tech/sendico/ledger/storage/model" + pmessaging "github.com/tech/sendico/pkg/messaging" + me "github.com/tech/sendico/pkg/messaging/envelope" + "github.com/tech/sendico/pkg/mlogger" + domainmodel "github.com/tech/sendico/pkg/model" + notification "github.com/tech/sendico/pkg/model/notification" + "github.com/tech/sendico/pkg/mservice" + "go.uber.org/zap" +) + +const ( + defaultOutboxBatchSize = 100 + defaultOutboxPollInterval = time.Second + maxOutboxDeliveryAttempts = 5 + outboxPublisherSender = "ledger.outbox.publisher" +) + +type outboxPublisher struct { + logger mlogger.Logger + store storage.OutboxStore + producer pmessaging.Producer + + batchSize int + pollInterval time.Duration +} + +func newOutboxPublisher(logger mlogger.Logger, store storage.OutboxStore, producer pmessaging.Producer) *outboxPublisher { + return &outboxPublisher{ + logger: logger.Named("outbox.publisher"), + store: store, + producer: producer, + batchSize: defaultOutboxBatchSize, + pollInterval: defaultOutboxPollInterval, + } +} + +func (p *outboxPublisher) run(ctx context.Context) { + p.logger.Info("started") + defer p.logger.Info("stopped") + + for { + if ctx.Err() != nil { + return + } + + processed, err := p.dispatchPending(ctx) + if err != nil && !errors.Is(err, context.Canceled) { + p.logger.Warn("failed to dispatch ledger outbox events", zap.Error(err)) + } + if processed > 0 { + p.logger.Debug("dispatched ledger outbox events", + zap.Int("count", processed), + zap.Int("batch_size", p.batchSize)) + } + + if ctx.Err() != nil { + return + } + + if processed == 0 { + select { + case <-ctx.Done(): + return + case <-time.After(p.pollInterval): + } + } + } +} + +func (p *outboxPublisher) dispatchPending(ctx context.Context) (int, error) { + if p.store == nil || p.producer == nil { + return 0, nil + } + + events, err := p.store.ListPending(ctx, p.batchSize) + if err != nil { + return 0, err + } + + for _, event := range events { + if ctx.Err() != nil { + return len(events), ctx.Err() + } + if err := p.publishEvent(ctx, event); err != nil { + if errors.Is(err, context.Canceled) { + return len(events), err + } + p.logger.Warn("failed to publish outbox event", + zap.Error(err), + zap.String("eventId", event.EventID), + zap.String("subject", event.Subject), + zap.String("organizationRef", event.OrganizationRef.Hex()), + zap.Int("attempts", event.Attempts)) + p.handleFailure(ctx, event) + continue + } + if err := p.markSent(ctx, event); err != nil { + if errors.Is(err, context.Canceled) { + return len(events), err + } + p.logger.Warn("failed to mark outbox event as sent", + zap.Error(err), + zap.String("eventId", event.EventID), + zap.String("subject", event.Subject), + zap.String("organizationRef", event.OrganizationRef.Hex())) + } else { + p.logger.Debug("outbox event marked sent", + zap.String("eventId", event.EventID), + zap.String("subject", event.Subject), + zap.String("organizationRef", event.OrganizationRef.Hex())) + } + } + + return len(events), nil +} + +func (p *outboxPublisher) publishEvent(_ context.Context, event *ledgerModel.OutboxEvent) error { + docID := event.GetID() + if docID == nil || docID.IsZero() { + return errors.New("outbox event missing identifier") + } + + payload, err := p.wrapPayload(event) + if err != nil { + return err + } + + env := me.CreateEnvelope(outboxPublisherSender, domainmodel.NewNotification(mservice.LedgerOutbox, notification.NASent)) + if _, err = env.Wrap(payload); err != nil { + return err + } + + return p.producer.SendMessage(env) +} + +func (p *outboxPublisher) wrapPayload(event *ledgerModel.OutboxEvent) ([]byte, error) { + message := ledgerOutboxMessage{ + EventID: event.EventID, + Subject: event.Subject, + Payload: json.RawMessage(event.Payload), + Attempts: event.Attempts, + OrganizationRef: event.OrganizationRef.Hex(), + CreatedAt: event.CreatedAt, + } + return json.Marshal(message) +} + +func (p *outboxPublisher) markSent(ctx context.Context, event *ledgerModel.OutboxEvent) error { + eventRef := event.GetID() + if eventRef == nil || eventRef.IsZero() { + return errors.New("outbox event missing identifier") + } + + return p.store.MarkSent(ctx, *eventRef, time.Now().UTC()) +} + +func (p *outboxPublisher) handleFailure(ctx context.Context, event *ledgerModel.OutboxEvent) { + eventRef := event.GetID() + if eventRef == nil || eventRef.IsZero() { + p.logger.Warn("cannot record outbox failure: missing identifier", zap.String("eventId", event.EventID)) + return + } + + if err := p.store.IncrementAttempts(ctx, *eventRef); err != nil && !errors.Is(err, context.Canceled) { + p.logger.Warn("failed to increment outbox attempts", + zap.Error(err), + zap.String("eventId", event.EventID), + zap.String("subject", event.Subject), + zap.String("organizationRef", event.OrganizationRef.Hex())) + } + + if event.Attempts+1 >= maxOutboxDeliveryAttempts { + if err := p.store.MarkFailed(ctx, *eventRef); err != nil && !errors.Is(err, context.Canceled) { + p.logger.Warn("failed to mark outbox event failed", + zap.Error(err), + zap.String("eventId", event.EventID), + zap.String("subject", event.Subject), + zap.String("organizationRef", event.OrganizationRef.Hex()), + zap.Int("attempts", event.Attempts+1)) + } else { + p.logger.Warn("ledger outbox event marked as failed", + zap.String("eventId", event.EventID), + zap.String("subject", event.Subject), + zap.String("organizationRef", event.OrganizationRef.Hex()), + zap.Int("attempts", event.Attempts+1)) + } + } +} + +type ledgerOutboxMessage struct { + EventID string `json:"eventId"` + Subject string `json:"subject"` + Payload json.RawMessage `json:"payload"` + Attempts int `json:"attempts"` + OrganizationRef string `json:"organizationRef"` + CreatedAt time.Time `json:"createdAt"` +} diff --git a/api/ledger/internal/service/ledger/outbox_publisher_test.go b/api/ledger/internal/service/ledger/outbox_publisher_test.go new file mode 100644 index 0000000..c9b0bba --- /dev/null +++ b/api/ledger/internal/service/ledger/outbox_publisher_test.go @@ -0,0 +1,142 @@ +package ledger + +import ( + "context" + "encoding/json" + "errors" + "sync" + "testing" + "time" + + "github.com/tech/sendico/ledger/storage/model" + me "github.com/tech/sendico/pkg/messaging/envelope" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestOutboxPublisherDispatchSuccess(t *testing.T) { + logger := zap.NewNop() + event := &model.OutboxEvent{ + EventID: "entry-1", + Subject: "ledger.entry.posted", + Payload: []byte(`{"journalEntryRef":"abc123"}`), + Attempts: 0, + } + event.SetID(primitive.NewObjectID()) + event.OrganizationRef = primitive.NewObjectID() + + store := &recordingOutboxStore{ + pending: []*model.OutboxEvent{event}, + } + producer := &stubProducer{} + publisher := newOutboxPublisher(logger, store, producer) + + processed, err := publisher.dispatchPending(context.Background()) + require.NoError(t, err) + assert.Equal(t, 1, processed) + + require.Len(t, producer.envelopes, 1) + env := producer.envelopes[0] + assert.Equal(t, outboxPublisherSender, env.GetSender()) + assert.Equal(t, "ledger_outbox_sent", env.GetSignature().ToString()) + + var message ledgerOutboxMessage + require.NoError(t, json.Unmarshal(env.GetData(), &message)) + assert.Equal(t, event.EventID, message.EventID) + assert.Equal(t, event.Subject, message.Subject) + assert.Equal(t, event.OrganizationRef.Hex(), message.OrganizationRef) + + require.Len(t, store.markedSent, 1) + assert.Equal(t, *event.GetID(), store.markedSent[0]) + assert.Empty(t, store.markedFailed) + assert.Empty(t, store.incremented) +} + +func TestOutboxPublisherDispatchFailureMarksAttempts(t *testing.T) { + logger := zap.NewNop() + event := &model.OutboxEvent{ + EventID: "entry-2", + Subject: "ledger.entry.posted", + Payload: []byte(`{"journalEntryRef":"xyz789"}`), + Attempts: maxOutboxDeliveryAttempts - 1, + } + event.SetID(primitive.NewObjectID()) + event.OrganizationRef = primitive.NewObjectID() + + store := &recordingOutboxStore{ + pending: []*model.OutboxEvent{event}, + } + producer := &stubProducer{err: errors.New("publish failed")} + publisher := newOutboxPublisher(logger, store, producer) + + processed, err := publisher.dispatchPending(context.Background()) + require.NoError(t, err) + assert.Equal(t, 1, processed) + + require.Len(t, store.incremented, 1) + assert.Equal(t, *event.GetID(), store.incremented[0]) + + require.Len(t, store.markedFailed, 1) + assert.Equal(t, *event.GetID(), store.markedFailed[0]) + + assert.Empty(t, store.markedSent) +} + +type recordingOutboxStore struct { + mu sync.Mutex + + pending []*model.OutboxEvent + + markedSent []primitive.ObjectID + markedFailed []primitive.ObjectID + incremented []primitive.ObjectID +} + +func (s *recordingOutboxStore) Create(context.Context, *model.OutboxEvent) error { + return nil +} + +func (s *recordingOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) { + s.mu.Lock() + defer s.mu.Unlock() + events := s.pending + s.pending = nil + return events, nil +} + +func (s *recordingOutboxStore) MarkSent(_ context.Context, eventRef primitive.ObjectID, sentAt time.Time) error { + _ = sentAt + s.mu.Lock() + defer s.mu.Unlock() + s.markedSent = append(s.markedSent, eventRef) + return nil +} + +func (s *recordingOutboxStore) MarkFailed(_ context.Context, eventRef primitive.ObjectID) error { + s.mu.Lock() + defer s.mu.Unlock() + s.markedFailed = append(s.markedFailed, eventRef) + return nil +} + +func (s *recordingOutboxStore) IncrementAttempts(_ context.Context, eventRef primitive.ObjectID) error { + s.mu.Lock() + defer s.mu.Unlock() + s.incremented = append(s.incremented, eventRef) + return nil +} + +type stubProducer struct { + mu sync.Mutex + envelopes []me.Envelope + err error +} + +func (p *stubProducer) SendMessage(env me.Envelope) error { + p.mu.Lock() + defer p.mu.Unlock() + p.envelopes = append(p.envelopes, env) + return p.err +} diff --git a/api/ledger/internal/service/ledger/posting.go b/api/ledger/internal/service/ledger/posting.go new file mode 100644 index 0000000..0492bcf --- /dev/null +++ b/api/ledger/internal/service/ledger/posting.go @@ -0,0 +1,239 @@ +package ledger + +import ( + "context" + "fmt" + "time" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + storageMongo "github.com/tech/sendico/ledger/storage/mongo" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +const ledgerOutboxSubject = "ledger.entry.posted" + +// postCreditResponder implements credit posting with charges +func (s *Service) postCreditResponder(_ context.Context, req *ledgerv1.PostCreditRequest) gsresponse.Responder[ledgerv1.PostResponse] { + return func(ctx context.Context) (*ledgerv1.PostResponse, error) { + if req.IdempotencyKey == "" { + return nil, merrors.InvalidArgument("idempotency_key is required") + } + if req.OrganizationRef == "" { + return nil, merrors.InvalidArgument("organization_ref is required") + } + if req.LedgerAccountRef == "" { + return nil, merrors.InvalidArgument("ledger_account_ref is required") + } + if err := validateMoney(req.Money, "money"); err != nil { + return nil, err + } + + orgRef, err := parseObjectID(req.OrganizationRef) + if err != nil { + return nil, err + } + accountRef, err := parseObjectID(req.LedgerAccountRef) + if err != nil { + return nil, err + } + + existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) + if err == nil && existingEntry != nil { + recordDuplicateRequest("credit") + s.logger.Info("duplicate credit request (idempotency)", + zap.String("idempotencyKey", req.IdempotencyKey), + zap.String("existingEntryID", existingEntry.GetID().Hex())) + return &ledgerv1.PostResponse{ + JournalEntryRef: existingEntry.GetID().Hex(), + Version: existingEntry.Version, + EntryType: ledgerv1.EntryType_ENTRY_CREDIT, + }, nil + } + if err != nil && err != storage.ErrJournalEntryNotFound { + recordJournalEntryError("credit", "idempotency_check_failed") + s.logger.Warn("failed to check idempotency", zap.Error(err)) + return nil, merrors.Internal("failed to check idempotency") + } + + account, err := s.storage.Accounts().Get(ctx, accountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + recordJournalEntryError("credit", "account_not_found") + return nil, merrors.NoData("account not found") + } + recordJournalEntryError("credit", "account_lookup_failed") + s.logger.Warn("failed to get account", zap.Error(err)) + return nil, merrors.Internal("failed to get account") + } + if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil { + recordJournalEntryError("credit", "account_invalid") + return nil, err + } + + accountsByRef := map[primitive.ObjectID]*model.Account{accountRef: account} + + eventTime := getEventTime(req.EventTime) + creditAmount, _ := parseDecimal(req.Money.Amount) + entryTotal := creditAmount + + charges := req.Charges + if len(charges) == 0 { + if computed, err := s.quoteFeesForCredit(ctx, req); err != nil { + s.logger.Warn("failed to quote fees", zap.Error(err)) + } else if len(computed) > 0 { + charges = computed + } + } + if err := validatePostingLines(charges); err != nil { + return nil, err + } + + postingLines := make([]*model.PostingLine, 0, 2+len(charges)) + mainLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: accountRef, + Amount: creditAmount.String(), + Currency: req.Money.Currency, + LineType: model.LineTypeMain, + } + mainLine.OrganizationRef = orgRef + postingLines = append(postingLines, mainLine) + + for i, charge := range charges { + chargeAccountRef, err := parseObjectID(charge.LedgerAccountRef) + if err != nil { + return nil, err + } + if charge.Money.Currency != req.Money.Currency { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: currency mismatch", i)) + } + + chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) + } + s.logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) + return nil, merrors.Internal("failed to get charge account") + } + if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: %s", i, err.Error())) + } + + chargeAmount, err := parseDecimal(charge.Money.Amount) + if err != nil { + return nil, err + } + entryTotal = entryTotal.Add(chargeAmount) + + chargeLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: chargeAccountRef, + Amount: chargeAmount.String(), + Currency: charge.Money.Currency, + LineType: protoLineTypeToModel(charge.LineType), + } + chargeLine.OrganizationRef = orgRef + postingLines = append(postingLines, chargeLine) + } + + contraAccount, err := s.resolveSettlementAccount(ctx, orgRef, req.Money.Currency, req.ContraLedgerAccountRef, accountsByRef) + if err != nil { + recordJournalEntryError("credit", "contra_resolve_failed") + return nil, err + } + contraAccountID := contraAccount.GetID() + if contraAccountID == nil { + recordJournalEntryError("credit", "contra_missing_id") + return nil, merrors.Internal("contra account missing identifier") + } + + contraAmount := entryTotal.Neg() + if !contraAmount.IsZero() || len(postingLines) == 1 { + contraLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: *contraAccountID, + Amount: contraAmount.String(), + Currency: req.Money.Currency, + LineType: model.LineTypeMain, + } + contraLine.OrganizationRef = orgRef + postingLines = append(postingLines, contraLine) + entryTotal = entryTotal.Add(contraAmount) + } + + if !entryTotal.IsZero() { + recordJournalEntryError("credit", "unbalanced_after_contra") + return nil, merrors.Internal("failed to balance journal entry") + } + + mongoStore, ok := s.storage.(*storageMongo.Store) + if !ok { + return nil, merrors.Internal("storage does not support transactions") + } + + result, err := mongoStore.TransactionFactory().CreateTransaction().Execute(ctx, func(txCtx context.Context) (any, error) { + entry := &model.JournalEntry{ + IdempotencyKey: req.IdempotencyKey, + EventTime: eventTime, + EntryType: model.EntryTypeCredit, + Description: req.Description, + Metadata: req.Metadata, + Version: time.Now().UnixNano(), + } + entry.OrganizationRef = orgRef + + if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { + s.logger.Warn("failed to create journal entry", zap.Error(err)) + return nil, merrors.Internal("failed to create journal entry") + } + + entryRef := entry.GetID() + if entryRef == nil { + return nil, merrors.Internal("journal entry missing identifier") + } + + for _, line := range postingLines { + line.JournalEntryRef = *entryRef + } + + if err := validateBalanced(postingLines); err != nil { + return nil, err + } + + if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { + s.logger.Warn("failed to create posting lines", zap.Error(err)) + return nil, merrors.Internal("failed to create posting lines") + } + + if err := s.upsertBalances(txCtx, postingLines, accountsByRef); err != nil { + return nil, err + } + + if err := s.enqueueOutbox(txCtx, entry, postingLines); err != nil { + return nil, err + } + + return &ledgerv1.PostResponse{ + JournalEntryRef: entryRef.Hex(), + Version: entry.Version, + EntryType: ledgerv1.EntryType_ENTRY_CREDIT, + }, nil + }) + + if err != nil { + recordJournalEntryError("credit", "transaction_failed") + return nil, err + } + + amountFloat, _ := creditAmount.Float64() + recordTransactionAmount(req.Money.Currency, "credit", amountFloat) + recordJournalEntry("credit", "success", 0) + return result.(*ledgerv1.PostResponse), nil + } +} diff --git a/api/ledger/internal/service/ledger/posting_debit.go b/api/ledger/internal/service/ledger/posting_debit.go new file mode 100644 index 0000000..717975f --- /dev/null +++ b/api/ledger/internal/service/ledger/posting_debit.go @@ -0,0 +1,233 @@ +package ledger + +import ( + "context" + "fmt" + "time" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + storageMongo "github.com/tech/sendico/ledger/storage/mongo" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// postDebitResponder implements debit posting with charges +func (s *Service) postDebitResponder(_ context.Context, req *ledgerv1.PostDebitRequest) gsresponse.Responder[ledgerv1.PostResponse] { + return func(ctx context.Context) (*ledgerv1.PostResponse, error) { + if req.IdempotencyKey == "" { + return nil, merrors.InvalidArgument("idempotency_key is required") + } + if req.OrganizationRef == "" { + return nil, merrors.InvalidArgument("organization_ref is required") + } + if req.LedgerAccountRef == "" { + return nil, merrors.InvalidArgument("ledger_account_ref is required") + } + if err := validateMoney(req.Money, "money"); err != nil { + return nil, err + } + + orgRef, err := parseObjectID(req.OrganizationRef) + if err != nil { + return nil, err + } + accountRef, err := parseObjectID(req.LedgerAccountRef) + if err != nil { + return nil, err + } + + existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) + if err == nil && existingEntry != nil { + recordDuplicateRequest("debit") + s.logger.Info("duplicate debit request (idempotency)", + zap.String("idempotencyKey", req.IdempotencyKey), + zap.String("existingEntryID", existingEntry.GetID().Hex())) + return &ledgerv1.PostResponse{ + JournalEntryRef: existingEntry.GetID().Hex(), + Version: existingEntry.Version, + EntryType: ledgerv1.EntryType_ENTRY_DEBIT, + }, nil + } + if err != nil && err != storage.ErrJournalEntryNotFound { + s.logger.Warn("failed to check idempotency", zap.Error(err)) + return nil, merrors.Internal("failed to check idempotency") + } + + account, err := s.storage.Accounts().Get(ctx, accountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("account not found") + } + s.logger.Warn("failed to get account", zap.Error(err)) + return nil, merrors.Internal("failed to get account") + } + if err := validateAccountForOrg(account, orgRef, req.Money.Currency); err != nil { + return nil, err + } + + accountsByRef := map[primitive.ObjectID]*model.Account{accountRef: account} + + eventTime := getEventTime(req.EventTime) + debitAmount, _ := parseDecimal(req.Money.Amount) + entryTotal := debitAmount.Neg() + + charges := req.Charges + if len(charges) == 0 { + if computed, err := s.quoteFeesForDebit(ctx, req); err != nil { + s.logger.Warn("failed to quote fees", zap.Error(err)) + } else if len(computed) > 0 { + charges = computed + } + } + if err := validatePostingLines(charges); err != nil { + return nil, err + } + + postingLines := make([]*model.PostingLine, 0, 2+len(charges)) + mainLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: accountRef, + Amount: debitAmount.Neg().String(), + Currency: req.Money.Currency, + LineType: model.LineTypeMain, + } + mainLine.OrganizationRef = orgRef + postingLines = append(postingLines, mainLine) + + for i, charge := range charges { + chargeAccountRef, err := parseObjectID(charge.LedgerAccountRef) + if err != nil { + return nil, err + } + if charge.Money.Currency != req.Money.Currency { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: currency mismatch", i)) + } + + chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) + } + s.logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) + return nil, merrors.Internal("failed to get charge account") + } + if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: %s", i, err.Error())) + } + + chargeAmount, err := parseDecimal(charge.Money.Amount) + if err != nil { + return nil, err + } + entryTotal = entryTotal.Add(chargeAmount) + + chargeLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: chargeAccountRef, + Amount: chargeAmount.String(), + Currency: charge.Money.Currency, + LineType: protoLineTypeToModel(charge.LineType), + } + chargeLine.OrganizationRef = orgRef + postingLines = append(postingLines, chargeLine) + } + + contraAccount, err := s.resolveSettlementAccount(ctx, orgRef, req.Money.Currency, req.ContraLedgerAccountRef, accountsByRef) + if err != nil { + recordJournalEntryError("debit", "contra_resolve_failed") + return nil, err + } + contraAccountID := contraAccount.GetID() + if contraAccountID == nil { + recordJournalEntryError("debit", "contra_missing_id") + return nil, merrors.Internal("contra account missing identifier") + } + + contraAmount := entryTotal.Neg() + if !contraAmount.IsZero() || len(postingLines) == 1 { + contraLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: *contraAccountID, + Amount: contraAmount.String(), + Currency: req.Money.Currency, + LineType: model.LineTypeMain, + } + contraLine.OrganizationRef = orgRef + postingLines = append(postingLines, contraLine) + entryTotal = entryTotal.Add(contraAmount) + } + + if !entryTotal.IsZero() { + recordJournalEntryError("debit", "unbalanced_after_contra") + return nil, merrors.Internal("failed to balance journal entry") + } + + mongoStore, ok := s.storage.(*storageMongo.Store) + if !ok { + return nil, merrors.Internal("storage does not support transactions") + } + + result, err := mongoStore.TransactionFactory().CreateTransaction().Execute(ctx, func(txCtx context.Context) (any, error) { + entry := &model.JournalEntry{ + IdempotencyKey: req.IdempotencyKey, + EventTime: eventTime, + EntryType: model.EntryTypeDebit, + Description: req.Description, + Metadata: req.Metadata, + Version: time.Now().UnixNano(), + } + entry.OrganizationRef = orgRef + + if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { + s.logger.Warn("failed to create journal entry", zap.Error(err)) + return nil, merrors.Internal("failed to create journal entry") + } + + entryRef := entry.GetID() + if entryRef == nil { + return nil, merrors.Internal("journal entry missing identifier") + } + + for _, line := range postingLines { + line.JournalEntryRef = *entryRef + } + + if err := validateBalanced(postingLines); err != nil { + return nil, err + } + + if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { + s.logger.Warn("failed to create posting lines", zap.Error(err)) + return nil, merrors.Internal("failed to create posting lines") + } + + if err := s.upsertBalances(txCtx, postingLines, accountsByRef); err != nil { + return nil, err + } + + if err := s.enqueueOutbox(txCtx, entry, postingLines); err != nil { + return nil, err + } + + return &ledgerv1.PostResponse{ + JournalEntryRef: entryRef.Hex(), + Version: entry.Version, + EntryType: ledgerv1.EntryType_ENTRY_DEBIT, + }, nil + }) + + if err != nil { + recordJournalEntryError("debit", "transaction_failed") + return nil, err + } + + amountFloat, _ := debitAmount.Float64() + recordTransactionAmount(req.Money.Currency, "debit", amountFloat) + recordJournalEntry("debit", "success", 0) + return result.(*ledgerv1.PostResponse), nil + } +} diff --git a/api/ledger/internal/service/ledger/posting_fx.go b/api/ledger/internal/service/ledger/posting_fx.go new file mode 100644 index 0000000..95e7ac3 --- /dev/null +++ b/api/ledger/internal/service/ledger/posting_fx.go @@ -0,0 +1,254 @@ +package ledger + +import ( + "context" + "fmt" + "time" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + storageMongo "github.com/tech/sendico/ledger/storage/mongo" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// fxResponder implements foreign exchange transactions with charges +func (s *Service) fxResponder(_ context.Context, req *ledgerv1.FXRequest) gsresponse.Responder[ledgerv1.PostResponse] { + return func(ctx context.Context) (*ledgerv1.PostResponse, error) { + // Validate request + if req.IdempotencyKey == "" { + return nil, merrors.InvalidArgument("idempotency_key is required") + } + if req.OrganizationRef == "" { + return nil, merrors.InvalidArgument("organization_ref is required") + } + if req.FromLedgerAccountRef == "" { + return nil, merrors.InvalidArgument("from_ledger_account_ref is required") + } + if req.ToLedgerAccountRef == "" { + return nil, merrors.InvalidArgument("to_ledger_account_ref is required") + } + if req.FromLedgerAccountRef == req.ToLedgerAccountRef { + return nil, merrors.InvalidArgument("cannot exchange to same account") + } + if err := validateMoney(req.FromMoney, "from_money"); err != nil { + return nil, err + } + if err := validateMoney(req.ToMoney, "to_money"); err != nil { + return nil, err + } + if req.FromMoney.Currency == req.ToMoney.Currency { + return nil, merrors.InvalidArgument("from_money and to_money must have different currencies") + } + if req.Rate == "" { + return nil, merrors.InvalidArgument("rate is required") + } + if err := validatePostingLines(req.Charges); err != nil { + return nil, err + } + + orgRef, err := parseObjectID(req.OrganizationRef) + if err != nil { + return nil, err + } + fromAccountRef, err := parseObjectID(req.FromLedgerAccountRef) + if err != nil { + return nil, err + } + toAccountRef, err := parseObjectID(req.ToLedgerAccountRef) + if err != nil { + return nil, err + } + + // Check for duplicate idempotency key + existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) + if err == nil && existingEntry != nil { + recordDuplicateRequest("fx") + s.logger.Info("duplicate FX request (idempotency)", + zap.String("idempotencyKey", req.IdempotencyKey), + zap.String("existingEntryID", existingEntry.GetID().Hex())) + return &ledgerv1.PostResponse{ + JournalEntryRef: existingEntry.GetID().Hex(), + Version: existingEntry.Version, + EntryType: ledgerv1.EntryType_ENTRY_FX, + }, nil + } + if err != nil && err != storage.ErrJournalEntryNotFound { + s.logger.Warn("failed to check idempotency", zap.Error(err)) + return nil, merrors.Internal("failed to check idempotency") + } + + // Verify both accounts exist and are active + fromAccount, err := s.storage.Accounts().Get(ctx, fromAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("from_account not found") + } + s.logger.Warn("failed to get from_account", zap.Error(err)) + return nil, merrors.Internal("failed to get from_account") + } + if err := validateAccountForOrg(fromAccount, orgRef, req.FromMoney.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("from_account: %s", err.Error())) + } + + toAccount, err := s.storage.Accounts().Get(ctx, toAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("to_account not found") + } + s.logger.Warn("failed to get to_account", zap.Error(err)) + return nil, merrors.Internal("failed to get to_account") + } + if err := validateAccountForOrg(toAccount, orgRef, req.ToMoney.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("to_account: %s", err.Error())) + } + + accountsByRef := map[primitive.ObjectID]*model.Account{ + fromAccountRef: fromAccount, + toAccountRef: toAccount, + } + + eventTime := getEventTime(req.EventTime) + fromAmount, _ := parseDecimal(req.FromMoney.Amount) + toAmount, _ := parseDecimal(req.ToMoney.Amount) + + // Create posting lines for FX + // Dr From Account in fromCurrency (debit = negative) + // Cr To Account in toCurrency (credit = positive) + postingLines := make([]*model.PostingLine, 0, 2+len(req.Charges)) + + // Debit from account + fromLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: fromAccountRef, + Amount: fromAmount.Neg().String(), // negative = debit + Currency: req.FromMoney.Currency, + LineType: model.LineTypeMain, + } + fromLine.OrganizationRef = orgRef + postingLines = append(postingLines, fromLine) + + // Credit to account + toLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: toAccountRef, + Amount: toAmount.String(), // positive = credit + Currency: req.ToMoney.Currency, + LineType: model.LineTypeMain, + } + toLine.OrganizationRef = orgRef + postingLines = append(postingLines, toLine) + + for i, charge := range req.Charges { + chargeAccountRef, err := parseObjectID(charge.LedgerAccountRef) + if err != nil { + return nil, err + } + + chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) + } + s.logger.Warn("failed to get FX charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) + return nil, merrors.Internal("failed to get charge account") + } + if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: %s", i, err.Error())) + } + + chargeAmount, err := parseDecimal(charge.Money.Amount) + if err != nil { + return nil, err + } + + chargeLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: chargeAccountRef, + Amount: chargeAmount.String(), + Currency: charge.Money.Currency, + LineType: protoLineTypeToModel(charge.LineType), + } + chargeLine.OrganizationRef = orgRef + postingLines = append(postingLines, chargeLine) + } + + // Execute in transaction + mongoStore, ok := s.storage.(*storageMongo.Store) + if !ok { + return nil, merrors.Internal("storage does not support transactions") + } + + result, err := mongoStore.TransactionFactory().CreateTransaction().Execute(ctx, func(txCtx context.Context) (any, error) { + metadata := make(map[string]string) + if req.Metadata != nil { + for k, v := range req.Metadata { + metadata[k] = v + } + } + metadata["fx_rate"] = req.Rate + metadata["from_currency"] = req.FromMoney.Currency + metadata["to_currency"] = req.ToMoney.Currency + metadata["from_amount"] = req.FromMoney.Amount + metadata["to_amount"] = req.ToMoney.Amount + + entry := &model.JournalEntry{ + IdempotencyKey: req.IdempotencyKey, + EventTime: eventTime, + EntryType: model.EntryTypeFX, + Description: req.Description, + Metadata: metadata, + Version: time.Now().UnixNano(), + } + entry.OrganizationRef = orgRef + + if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { + s.logger.Warn("failed to create journal entry", zap.Error(err)) + return nil, merrors.Internal("failed to create journal entry") + } + + entryRef := entry.GetID() + if entryRef == nil { + return nil, merrors.Internal("journal entry missing identifier") + } + + for _, line := range postingLines { + line.JournalEntryRef = *entryRef + } + + if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { + s.logger.Warn("failed to create posting lines", zap.Error(err)) + return nil, merrors.Internal("failed to create posting lines") + } + + if err := s.upsertBalances(txCtx, postingLines, accountsByRef); err != nil { + return nil, err + } + + if err := s.enqueueOutbox(txCtx, entry, postingLines); err != nil { + return nil, err + } + + return &ledgerv1.PostResponse{ + JournalEntryRef: entryRef.Hex(), + Version: entry.Version, + EntryType: ledgerv1.EntryType_ENTRY_FX, + }, nil + }) + + if err != nil { + recordJournalEntryError("fx", "transaction_failed") + return nil, err + } + + fromAmountFloat, _ := fromAmount.Float64() + toAmountFloat, _ := toAmount.Float64() + recordTransactionAmount(req.FromMoney.Currency, "fx", fromAmountFloat) + recordTransactionAmount(req.ToMoney.Currency, "fx", toAmountFloat) + recordJournalEntry("fx", "success", 0) + return result.(*ledgerv1.PostResponse), nil + } +} diff --git a/api/ledger/internal/service/ledger/posting_support.go b/api/ledger/internal/service/ledger/posting_support.go new file mode 100644 index 0000000..d3ff4a7 --- /dev/null +++ b/api/ledger/internal/service/ledger/posting_support.go @@ -0,0 +1,228 @@ +package ledger + +import ( + "context" + "encoding/json" + "errors" + "fmt" + "time" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/merrors" + "github.com/shopspring/decimal" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +type outboxLinePayload struct { + AccountRef string `json:"accountRef"` + Amount string `json:"amount"` + Currency string `json:"currency"` + LineType string `json:"lineType"` +} + +type outboxJournalPayload struct { + JournalEntryRef string `json:"journalEntryRef"` + EntryType string `json:"entryType"` + OrganizationRef string `json:"organizationRef"` + Version int64 `json:"version"` + EventTime time.Time `json:"eventTime"` + Lines []outboxLinePayload `json:"lines"` +} + +func validateAccountForOrg(account *model.Account, orgRef primitive.ObjectID, currency string) error { + if account == nil { + return merrors.InvalidArgument("account is required") + } + if account.OrganizationRef != orgRef { + return merrors.InvalidArgument("account does not belong to organization") + } + if account.Status != model.AccountStatusActive { + return merrors.InvalidArgument(fmt.Sprintf("account is %s", account.Status)) + } + if currency != "" && account.Currency != currency { + return merrors.InvalidArgument(fmt.Sprintf("account currency mismatch: account=%s, expected=%s", account.Currency, currency)) + } + return nil +} + +func (s *Service) getAccount(ctx context.Context, cache map[primitive.ObjectID]*model.Account, accountRef primitive.ObjectID) (*model.Account, error) { + if accountRef.IsZero() { + return nil, merrors.InvalidArgument("account reference is required") + } + if account, ok := cache[accountRef]; ok { + return account, nil + } + + account, err := s.storage.Accounts().Get(ctx, accountRef) + if err != nil { + return nil, err + } + cache[accountRef] = account + return account, nil +} + +func (s *Service) resolveSettlementAccount(ctx context.Context, orgRef primitive.ObjectID, currency, override string, cache map[primitive.ObjectID]*model.Account) (*model.Account, error) { + if override != "" { + overrideRef, err := parseObjectID(override) + if err != nil { + return nil, err + } + account, err := s.getAccount(ctx, cache, overrideRef) + if err != nil { + if errors.Is(err, storage.ErrAccountNotFound) { + return nil, merrors.NoData("contra account not found") + } + s.logger.Warn("failed to load override contra account", zap.Error(err), zap.String("accountRef", overrideRef.Hex())) + return nil, merrors.Internal("failed to load contra account") + } + if err := validateAccountForOrg(account, orgRef, currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("contra account: %s", err.Error())) + } + return account, nil + } + + account, err := s.storage.Accounts().GetDefaultSettlement(ctx, orgRef, currency) + if err != nil { + if errors.Is(err, storage.ErrAccountNotFound) { + return nil, merrors.InvalidArgument("no default settlement account configured for currency") + } + s.logger.Warn("failed to resolve default settlement account", + zap.Error(err), + zap.String("organizationRef", orgRef.Hex()), + zap.String("currency", currency)) + return nil, merrors.Internal("failed to resolve settlement account") + } + + accountID := account.GetID() + if accountID == nil { + return nil, merrors.Internal("settlement account missing identifier") + } + cache[*accountID] = account + + if err := validateAccountForOrg(account, orgRef, currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("settlement account: %s", err.Error())) + } + + return account, nil +} + +func (s *Service) upsertBalances(ctx context.Context, lines []*model.PostingLine, accounts map[primitive.ObjectID]*model.Account) error { + if len(lines) == 0 { + return nil + } + + balanceDeltas := make(map[primitive.ObjectID]decimal.Decimal, len(lines)) + for _, line := range lines { + delta, err := parseDecimal(line.Amount) + if err != nil { + return err + } + if current, ok := balanceDeltas[line.AccountRef]; ok { + balanceDeltas[line.AccountRef] = current.Add(delta) + continue + } + balanceDeltas[line.AccountRef] = delta + } + + balancesStore := s.storage.Balances() + now := time.Now().UTC() + + for accountRef, delta := range balanceDeltas { + account := accounts[accountRef] + if account == nil { + s.logger.Warn("account cache missing for balance update", zap.String("accountRef", accountRef.Hex())) + return merrors.Internal("account cache missing for balance update") + } + + currentBalance, err := balancesStore.Get(ctx, accountRef) + if err != nil && !errors.Is(err, storage.ErrBalanceNotFound) { + s.logger.Warn("failed to fetch account balance", + zap.Error(err), + zap.String("accountRef", accountRef.Hex())) + return merrors.Internal("failed to update balance") + } + + newAmount := delta + version := int64(1) + if currentBalance != nil { + existing, err := parseDecimal(currentBalance.Balance) + if err != nil { + return err + } + newAmount = existing.Add(delta) + version = currentBalance.Version + 1 + } + + if !account.AllowNegative && newAmount.LessThan(decimal.Zero) { + return merrors.InvalidArgument(fmt.Sprintf("account %s does not allow negative balances", accountRef.Hex())) + } + + newBalance := &model.AccountBalance{ + AccountRef: accountRef, + Balance: newAmount.String(), + Currency: account.Currency, + Version: version, + LastUpdated: now, + } + newBalance.OrganizationRef = account.OrganizationRef + + if err := balancesStore.Upsert(ctx, newBalance); err != nil { + s.logger.Warn("failed to upsert account balance", zap.Error(err), zap.String("accountRef", accountRef.Hex())) + return merrors.Internal("failed to update balance") + } + } + + return nil +} + +func (s *Service) enqueueOutbox(ctx context.Context, entry *model.JournalEntry, lines []*model.PostingLine) error { + if entry == nil { + return merrors.Internal("journal entry is required") + } + entryID := entry.GetID() + if entryID == nil { + return merrors.Internal("journal entry missing identifier") + } + + payload := outboxJournalPayload{ + JournalEntryRef: entryID.Hex(), + EntryType: string(entry.EntryType), + OrganizationRef: entry.OrganizationRef.Hex(), + Version: entry.Version, + EventTime: entry.EventTime, + Lines: make([]outboxLinePayload, 0, len(lines)), + } + + for _, line := range lines { + payload.Lines = append(payload.Lines, outboxLinePayload{ + AccountRef: line.AccountRef.Hex(), + Amount: line.Amount, + Currency: line.Currency, + LineType: string(line.LineType), + }) + } + + body, err := json.Marshal(payload) + if err != nil { + s.logger.Warn("failed to marshal ledger outbox payload", zap.Error(err)) + return merrors.Internal("failed to marshal ledger event") + } + + event := &model.OutboxEvent{ + EventID: entryID.Hex(), + Subject: ledgerOutboxSubject, + Payload: body, + Status: model.OutboxStatusPending, + Attempts: 0, + } + event.OrganizationRef = entry.OrganizationRef + + if err := s.storage.Outbox().Create(ctx, event); err != nil { + s.logger.Warn("failed to enqueue ledger outbox event", zap.Error(err)) + return merrors.Internal("failed to enqueue ledger event") + } + + return nil +} diff --git a/api/ledger/internal/service/ledger/posting_support_test.go b/api/ledger/internal/service/ledger/posting_support_test.go new file mode 100644 index 0000000..1cc3731 --- /dev/null +++ b/api/ledger/internal/service/ledger/posting_support_test.go @@ -0,0 +1,282 @@ +package ledger + +import ( + "context" + "encoding/json" + "errors" + "testing" + "time" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/merrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +type stubRepository struct { + accounts storage.AccountsStore + balances storage.BalancesStore + outbox storage.OutboxStore +} + +func (s *stubRepository) Ping(context.Context) error { return nil } +func (s *stubRepository) Accounts() storage.AccountsStore { return s.accounts } +func (s *stubRepository) JournalEntries() storage.JournalEntriesStore { return nil } +func (s *stubRepository) PostingLines() storage.PostingLinesStore { return nil } +func (s *stubRepository) Balances() storage.BalancesStore { return s.balances } +func (s *stubRepository) Outbox() storage.OutboxStore { return s.outbox } + +type stubAccountsStore struct { + getByID map[primitive.ObjectID]*model.Account + defaultSettlement *model.Account + getErr error + defaultErr error +} + +func (s *stubAccountsStore) Create(context.Context, *model.Account) error { + return merrors.NotImplemented("create") +} +func (s *stubAccountsStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.Account, error) { + if s.getErr != nil { + return nil, s.getErr + } + if acc, ok := s.getByID[accountRef]; ok { + return acc, nil + } + return nil, storage.ErrAccountNotFound +} +func (s *stubAccountsStore) GetByAccountCode(context.Context, primitive.ObjectID, string, string) (*model.Account, error) { + return nil, merrors.NotImplemented("get by code") +} +func (s *stubAccountsStore) GetDefaultSettlement(context.Context, primitive.ObjectID, string) (*model.Account, error) { + if s.defaultErr != nil { + return nil, s.defaultErr + } + if s.defaultSettlement == nil { + return nil, storage.ErrAccountNotFound + } + return s.defaultSettlement, nil +} +func (s *stubAccountsStore) ListByOrganization(context.Context, primitive.ObjectID, int, int) ([]*model.Account, error) { + return nil, merrors.NotImplemented("list") +} +func (s *stubAccountsStore) UpdateStatus(context.Context, primitive.ObjectID, model.AccountStatus) error { + return merrors.NotImplemented("update status") +} + +type stubBalancesStore struct { + records map[primitive.ObjectID]*model.AccountBalance + upserts []*model.AccountBalance + getErr error + upErr error +} + +func (s *stubBalancesStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.AccountBalance, error) { + if s.getErr != nil { + return nil, s.getErr + } + if balance, ok := s.records[accountRef]; ok { + return balance, nil + } + return nil, storage.ErrBalanceNotFound +} + +func (s *stubBalancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error { + if s.upErr != nil { + return s.upErr + } + copied := *balance + s.upserts = append(s.upserts, &copied) + if s.records == nil { + s.records = make(map[primitive.ObjectID]*model.AccountBalance) + } + s.records[balance.AccountRef] = &copied + return nil +} + +func (s *stubBalancesStore) IncrementBalance(context.Context, primitive.ObjectID, string) error { + return merrors.NotImplemented("increment") +} + +type stubOutboxStore struct { + created []*model.OutboxEvent + err error +} + +func (s *stubOutboxStore) Create(ctx context.Context, event *model.OutboxEvent) error { + if s.err != nil { + return s.err + } + copied := *event + s.created = append(s.created, &copied) + return nil +} + +func (s *stubOutboxStore) ListPending(context.Context, int) ([]*model.OutboxEvent, error) { + return nil, merrors.NotImplemented("list") +} + +func (s *stubOutboxStore) MarkSent(context.Context, primitive.ObjectID, time.Time) error { + return merrors.NotImplemented("mark sent") +} + +func (s *stubOutboxStore) MarkFailed(context.Context, primitive.ObjectID) error { + return merrors.NotImplemented("mark failed") +} + +func (s *stubOutboxStore) IncrementAttempts(context.Context, primitive.ObjectID) error { + return merrors.NotImplemented("increment attempts") +} + +func TestResolveSettlementAccount_Default(t *testing.T) { + ctx := context.Background() + orgRef := primitive.NewObjectID() + settlementID := primitive.NewObjectID() + settlement := &model.Account{} + settlement.SetID(settlementID) + settlement.OrganizationRef = orgRef + settlement.Currency = "USD" + settlement.Status = model.AccountStatusActive + + accounts := &stubAccountsStore{defaultSettlement: settlement} + repo := &stubRepository{accounts: accounts} + service := &Service{logger: zap.NewNop(), storage: repo} + cache := make(map[primitive.ObjectID]*model.Account) + + result, err := service.resolveSettlementAccount(ctx, orgRef, "USD", "", cache) + + require.NoError(t, err) + assert.Equal(t, settlement, result) + assert.Equal(t, settlement, cache[settlementID]) +} + +func TestResolveSettlementAccount_Override(t *testing.T) { + ctx := context.Background() + orgRef := primitive.NewObjectID() + overrideID := primitive.NewObjectID() + override := &model.Account{} + override.SetID(overrideID) + override.OrganizationRef = orgRef + override.Currency = "EUR" + override.Status = model.AccountStatusActive + + accounts := &stubAccountsStore{getByID: map[primitive.ObjectID]*model.Account{overrideID: override}} + repo := &stubRepository{accounts: accounts} + service := &Service{logger: zap.NewNop(), storage: repo} + cache := make(map[primitive.ObjectID]*model.Account) + + result, err := service.resolveSettlementAccount(ctx, orgRef, "EUR", overrideID.Hex(), cache) + + require.NoError(t, err) + assert.Equal(t, override, result) + assert.Equal(t, override, cache[overrideID]) +} + +func TestResolveSettlementAccount_NoDefault(t *testing.T) { + ctx := context.Background() + orgRef := primitive.NewObjectID() + accounts := &stubAccountsStore{defaultErr: storage.ErrAccountNotFound} + repo := &stubRepository{accounts: accounts} + service := &Service{logger: zap.NewNop(), storage: repo} + + _, err := service.resolveSettlementAccount(ctx, orgRef, "USD", "", map[primitive.ObjectID]*model.Account{}) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) +} + +func TestUpsertBalances_Succeeds(t *testing.T) { + ctx := context.Background() + orgRef := primitive.NewObjectID() + accountRef := primitive.NewObjectID() + account := &model.Account{AllowNegative: false, Currency: "USD"} + account.OrganizationRef = orgRef + + balanceLines := []*model.PostingLine{ + { + AccountRef: accountRef, + Amount: "50", + Currency: "USD", + }, + } + + balances := &stubBalancesStore{} + repo := &stubRepository{balances: balances} + service := &Service{logger: zap.NewNop(), storage: repo} + accountCache := map[primitive.ObjectID]*model.Account{accountRef: account} + + require.NoError(t, service.upsertBalances(ctx, balanceLines, accountCache)) + require.Len(t, balances.upserts, 1) + assert.Equal(t, "50", balances.upserts[0].Balance) + assert.Equal(t, int64(1), balances.upserts[0].Version) + assert.Equal(t, "USD", balances.upserts[0].Currency) +} + +func TestUpsertBalances_DisallowNegative(t *testing.T) { + ctx := context.Background() + orgRef := primitive.NewObjectID() + accountRef := primitive.NewObjectID() + account := &model.Account{AllowNegative: false, Currency: "USD"} + account.OrganizationRef = orgRef + + balanceLines := []*model.PostingLine{ + { + AccountRef: accountRef, + Amount: "-10", + Currency: "USD", + }, + } + + balances := &stubBalancesStore{} + repo := &stubRepository{balances: balances} + service := &Service{logger: zap.NewNop(), storage: repo} + accountCache := map[primitive.ObjectID]*model.Account{accountRef: account} + + err := service.upsertBalances(ctx, balanceLines, accountCache) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) +} + +func TestEnqueueOutbox_CreatesEvent(t *testing.T) { + ctx := context.Background() + orgRef := primitive.NewObjectID() + entryID := primitive.NewObjectID() + entry := &model.JournalEntry{ + IdempotencyKey: "idem", + EventTime: time.Now().UTC(), + EntryType: model.EntryTypeCredit, + Version: 42, + } + entry.OrganizationRef = orgRef + entry.SetID(entryID) + + lines := []*model.PostingLine{ + { + AccountRef: primitive.NewObjectID(), + Amount: "100", + Currency: "USD", + LineType: model.LineTypeMain, + }, + } + + producer := &stubOutboxStore{} + repo := &stubRepository{outbox: producer} + service := &Service{logger: zap.NewNop(), storage: repo} + + require.NoError(t, service.enqueueOutbox(ctx, entry, lines)) + require.Len(t, producer.created, 1) + event := producer.created[0] + assert.Equal(t, entryID.Hex(), event.EventID) + assert.Equal(t, ledgerOutboxSubject, event.Subject) + + var payload outboxJournalPayload + require.NoError(t, json.Unmarshal(event.Payload, &payload)) + assert.Equal(t, entryID.Hex(), payload.JournalEntryRef) + assert.Equal(t, "credit", payload.EntryType) + assert.Len(t, payload.Lines, 1) + assert.Equal(t, "100", payload.Lines[0].Amount) +} diff --git a/api/ledger/internal/service/ledger/posting_transfer.go b/api/ledger/internal/service/ledger/posting_transfer.go new file mode 100644 index 0000000..b17e990 --- /dev/null +++ b/api/ledger/internal/service/ledger/posting_transfer.go @@ -0,0 +1,238 @@ +package ledger + +import ( + "context" + "fmt" + "time" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + storageMongo "github.com/tech/sendico/ledger/storage/mongo" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// transferResponder implements internal transfer between accounts +func (s *Service) transferResponder(_ context.Context, req *ledgerv1.TransferRequest) gsresponse.Responder[ledgerv1.PostResponse] { + return func(ctx context.Context) (*ledgerv1.PostResponse, error) { + // Validate request + if req.IdempotencyKey == "" { + return nil, merrors.InvalidArgument("idempotency_key is required") + } + if req.OrganizationRef == "" { + return nil, merrors.InvalidArgument("organization_ref is required") + } + if req.FromLedgerAccountRef == "" { + return nil, merrors.InvalidArgument("from_ledger_account_ref is required") + } + if req.ToLedgerAccountRef == "" { + return nil, merrors.InvalidArgument("to_ledger_account_ref is required") + } + if req.FromLedgerAccountRef == req.ToLedgerAccountRef { + return nil, merrors.InvalidArgument("cannot transfer to same account") + } + if err := validateMoney(req.Money, "money"); err != nil { + return nil, err + } + if err := validatePostingLines(req.Charges); err != nil { + return nil, err + } + + orgRef, err := parseObjectID(req.OrganizationRef) + if err != nil { + return nil, err + } + fromAccountRef, err := parseObjectID(req.FromLedgerAccountRef) + if err != nil { + return nil, err + } + toAccountRef, err := parseObjectID(req.ToLedgerAccountRef) + if err != nil { + return nil, err + } + + // Check for duplicate idempotency key + existingEntry, err := s.storage.JournalEntries().GetByIdempotencyKey(ctx, orgRef, req.IdempotencyKey) + if err == nil && existingEntry != nil { + recordDuplicateRequest("transfer") + s.logger.Info("duplicate transfer request (idempotency)", + zap.String("idempotencyKey", req.IdempotencyKey), + zap.String("existingEntryID", existingEntry.GetID().Hex())) + return &ledgerv1.PostResponse{ + JournalEntryRef: existingEntry.GetID().Hex(), + Version: existingEntry.Version, + EntryType: ledgerv1.EntryType_ENTRY_TRANSFER, + }, nil + } + if err != nil && err != storage.ErrJournalEntryNotFound { + s.logger.Warn("failed to check idempotency", zap.Error(err)) + return nil, merrors.Internal("failed to check idempotency") + } + + // Verify both accounts exist and are active + fromAccount, err := s.storage.Accounts().Get(ctx, fromAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("from_account not found") + } + s.logger.Warn("failed to get from_account", zap.Error(err)) + return nil, merrors.Internal("failed to get from_account") + } + if err := validateAccountForOrg(fromAccount, orgRef, req.Money.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("from_account: %s", err.Error())) + } + + toAccount, err := s.storage.Accounts().Get(ctx, toAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("to_account not found") + } + s.logger.Warn("failed to get to_account", zap.Error(err)) + return nil, merrors.Internal("failed to get to_account") + } + if err := validateAccountForOrg(toAccount, orgRef, req.Money.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("to_account: %s", err.Error())) + } + + accountsByRef := map[primitive.ObjectID]*model.Account{ + fromAccountRef: fromAccount, + toAccountRef: toAccount, + } + + eventTime := getEventTime(req.EventTime) + transferAmount, _ := parseDecimal(req.Money.Amount) + + // Create posting lines for transfer + // Dr From Account (debit = negative) + // Cr To Account (credit = positive) + postingLines := make([]*model.PostingLine, 0, 2+len(req.Charges)) + + // Debit from account + fromLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: fromAccountRef, + Amount: transferAmount.Neg().String(), // negative = debit + Currency: req.Money.Currency, + LineType: model.LineTypeMain, + } + fromLine.OrganizationRef = orgRef + postingLines = append(postingLines, fromLine) + + // Credit to account + toLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: toAccountRef, + Amount: transferAmount.String(), // positive = credit + Currency: req.Money.Currency, + LineType: model.LineTypeMain, + } + toLine.OrganizationRef = orgRef + postingLines = append(postingLines, toLine) + + // Process charges (fees/spreads) + for i, charge := range req.Charges { + chargeAccountRef, err := parseObjectID(charge.LedgerAccountRef) + if err != nil { + return nil, err + } + if charge.Money.Currency != req.Money.Currency { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: currency mismatch", i)) + } + + chargeAccount, err := s.getAccount(ctx, accountsByRef, chargeAccountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData(fmt.Sprintf("charges[%d]: account not found", i)) + } + s.logger.Warn("failed to get charge account", zap.Error(err), zap.String("chargeAccountRef", chargeAccountRef.Hex())) + return nil, merrors.Internal("failed to get charge account") + } + if err := validateAccountForOrg(chargeAccount, orgRef, charge.Money.Currency); err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("charges[%d]: %s", i, err.Error())) + } + + chargeAmount, err := parseDecimal(charge.Money.Amount) + if err != nil { + return nil, err + } + + chargeLine := &model.PostingLine{ + JournalEntryRef: primitive.NilObjectID, + AccountRef: chargeAccountRef, + Amount: chargeAmount.String(), + Currency: charge.Money.Currency, + LineType: protoLineTypeToModel(charge.LineType), + } + chargeLine.OrganizationRef = orgRef + postingLines = append(postingLines, chargeLine) + } + + // Execute in transaction + mongoStore, ok := s.storage.(*storageMongo.Store) + if !ok { + return nil, merrors.Internal("storage does not support transactions") + } + + result, err := mongoStore.TransactionFactory().CreateTransaction().Execute(ctx, func(txCtx context.Context) (any, error) { + entry := &model.JournalEntry{ + IdempotencyKey: req.IdempotencyKey, + EventTime: eventTime, + EntryType: model.EntryTypeTransfer, + Description: req.Description, + Metadata: req.Metadata, + Version: time.Now().UnixNano(), + } + entry.OrganizationRef = orgRef + + if err := s.storage.JournalEntries().Create(txCtx, entry); err != nil { + s.logger.Warn("failed to create journal entry", zap.Error(err)) + return nil, merrors.Internal("failed to create journal entry") + } + + entryRef := entry.GetID() + if entryRef == nil { + return nil, merrors.Internal("journal entry missing identifier") + } + + for _, line := range postingLines { + line.JournalEntryRef = *entryRef + } + + if err := validateBalanced(postingLines); err != nil { + return nil, err + } + + if err := s.storage.PostingLines().CreateMany(txCtx, postingLines); err != nil { + s.logger.Warn("failed to create posting lines", zap.Error(err)) + return nil, merrors.Internal("failed to create posting lines") + } + + if err := s.upsertBalances(txCtx, postingLines, accountsByRef); err != nil { + return nil, err + } + + if err := s.enqueueOutbox(txCtx, entry, postingLines); err != nil { + return nil, err + } + + return &ledgerv1.PostResponse{ + JournalEntryRef: entryRef.Hex(), + Version: entry.Version, + EntryType: ledgerv1.EntryType_ENTRY_TRANSFER, + }, nil + }) + + if err != nil { + recordJournalEntryError("transfer", "failed") + return nil, err + } + + amountFloat, _ := transferAmount.Float64() + recordTransactionAmount(req.Money.Currency, "transfer", amountFloat) + recordJournalEntry("transfer", "success", 0) + return result.(*ledgerv1.PostResponse), nil + } +} diff --git a/api/ledger/internal/service/ledger/queries.go b/api/ledger/internal/service/ledger/queries.go new file mode 100644 index 0000000..6e1a681 --- /dev/null +++ b/api/ledger/internal/service/ledger/queries.go @@ -0,0 +1,269 @@ +package ledger + +import ( + "context" + "encoding/base64" + "fmt" + "strconv" + "strings" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" +) + +// getBalanceResponder implements balance query logic +func (s *Service) getBalanceResponder(_ context.Context, req *ledgerv1.GetBalanceRequest) gsresponse.Responder[ledgerv1.BalanceResponse] { + return func(ctx context.Context) (*ledgerv1.BalanceResponse, error) { + if req.LedgerAccountRef == "" { + return nil, merrors.InvalidArgument("ledger_account_ref is required") + } + + accountRef, err := parseObjectID(req.LedgerAccountRef) + if err != nil { + return nil, err + } + + // Get account to verify it exists + account, err := s.storage.Accounts().Get(ctx, accountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("account not found") + } + s.logger.Warn("failed to get account", zap.Error(err)) + return nil, merrors.Internal("failed to get account") + } + + // Get balance + balance, err := s.storage.Balances().Get(ctx, accountRef) + if err != nil { + if err == storage.ErrBalanceNotFound { + // Return zero balance if account exists but has no balance yet + return &ledgerv1.BalanceResponse{ + LedgerAccountRef: req.LedgerAccountRef, + Balance: &moneyv1.Money{ + Amount: "0", + Currency: account.Currency, + }, + Version: 0, + LastUpdated: timestamppb.Now(), + }, nil + } + s.logger.Warn("failed to get balance", zap.Error(err)) + return nil, merrors.Internal("failed to get balance") + } + + recordBalanceQuery("success", 0) + + return &ledgerv1.BalanceResponse{ + LedgerAccountRef: req.LedgerAccountRef, + Balance: &moneyv1.Money{ + Amount: balance.Balance, + Currency: account.Currency, + }, + Version: balance.Version, + LastUpdated: timestamppb.New(balance.UpdatedAt), + }, nil + } +} + +// getJournalEntryResponder implements journal entry query logic +func (s *Service) getJournalEntryResponder(_ context.Context, req *ledgerv1.GetEntryRequest) gsresponse.Responder[ledgerv1.JournalEntryResponse] { + return func(ctx context.Context) (*ledgerv1.JournalEntryResponse, error) { + if req.EntryRef == "" { + return nil, merrors.InvalidArgument("entry_ref is required") + } + + entryRef, err := parseObjectID(req.EntryRef) + if err != nil { + return nil, err + } + + // Get journal entry + entry, err := s.storage.JournalEntries().Get(ctx, entryRef) + if err != nil { + if err == storage.ErrJournalEntryNotFound { + return nil, merrors.NoData("journal entry not found") + } + s.logger.Warn("failed to get journal entry", zap.Error(err)) + return nil, merrors.Internal("failed to get journal entry") + } + + // Get posting lines for this entry + lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef) + if err != nil { + s.logger.Warn("failed to get posting lines", zap.Error(err)) + return nil, merrors.Internal("failed to get posting lines") + } + + // Convert to proto + protoLines := make([]*ledgerv1.PostingLine, 0, len(lines)) + accountRefs := make([]string, 0, len(lines)) + for _, line := range lines { + protoLines = append(protoLines, &ledgerv1.PostingLine{ + LedgerAccountRef: line.AccountRef.Hex(), + Money: &moneyv1.Money{ + Amount: line.Amount, + Currency: line.Currency, + }, + LineType: modelLineTypeToProto(line.LineType), + }) + accountRefs = append(accountRefs, line.AccountRef.Hex()) + } + + return &ledgerv1.JournalEntryResponse{ + EntryRef: req.EntryRef, + IdempotencyKey: entry.IdempotencyKey, + EntryType: modelEntryTypeToProto(entry.EntryType), + Description: entry.Description, + EventTime: timestamppb.New(entry.EventTime), + Version: entry.Version, + Lines: protoLines, + Metadata: entry.Metadata, + LedgerAccountRefs: accountRefs, + }, nil + } +} + +// getStatementResponder implements account statement query logic +func (s *Service) getStatementResponder(_ context.Context, req *ledgerv1.GetStatementRequest) gsresponse.Responder[ledgerv1.StatementResponse] { + return func(ctx context.Context) (*ledgerv1.StatementResponse, error) { + if req.LedgerAccountRef == "" { + return nil, merrors.InvalidArgument("ledger_account_ref is required") + } + + accountRef, err := parseObjectID(req.LedgerAccountRef) + if err != nil { + return nil, err + } + + // Verify account exists + _, err = s.storage.Accounts().Get(ctx, accountRef) + if err != nil { + if err == storage.ErrAccountNotFound { + return nil, merrors.NoData("account not found") + } + s.logger.Warn("failed to get account", zap.Error(err)) + return nil, merrors.Internal("failed to get account") + } + + // Parse pagination + limit := int(req.Limit) + if limit <= 0 { + limit = 50 // default + } + if limit > 100 { + limit = 100 // max + } + + offset := 0 + if req.Cursor != "" { + offset, err = parseCursor(req.Cursor) + if err != nil { + return nil, merrors.InvalidArgument(fmt.Sprintf("invalid cursor: %v", err)) + } + } + + // Get posting lines for account + postingLines, err := s.storage.PostingLines().ListByAccount(ctx, accountRef, limit+1, offset) + if err != nil { + s.logger.Warn("failed to get posting lines", zap.Error(err)) + return nil, merrors.Internal("failed to get posting lines") + } + + // Check if there are more results + hasMore := len(postingLines) > limit + if hasMore { + postingLines = postingLines[:limit] + } + + // Group by journal entry and fetch entry details + entryMap := make(map[string]bool) + for _, line := range postingLines { + entryMap[line.JournalEntryRef.Hex()] = true + } + + entries := make([]*ledgerv1.JournalEntryResponse, 0) + for entryRefHex := range entryMap { + entryRef, _ := parseObjectID(entryRefHex) + + entry, err := s.storage.JournalEntries().Get(ctx, entryRef) + if err != nil { + s.logger.Warn("failed to get journal entry for statement", zap.Error(err), zap.String("entryRef", entryRefHex)) + continue + } + + // Get all lines for this entry + lines, err := s.storage.PostingLines().ListByJournalEntry(ctx, entryRef) + if err != nil { + s.logger.Warn("failed to get posting lines for entry", zap.Error(err), zap.String("entryRef", entryRefHex)) + continue + } + + // Convert to proto + protoLines := make([]*ledgerv1.PostingLine, 0, len(lines)) + accountRefs := make([]string, 0, len(lines)) + for _, line := range lines { + protoLines = append(protoLines, &ledgerv1.PostingLine{ + LedgerAccountRef: line.AccountRef.Hex(), + Money: &moneyv1.Money{ + Amount: line.Amount, + Currency: line.Currency, + }, + LineType: modelLineTypeToProto(line.LineType), + }) + accountRefs = append(accountRefs, line.AccountRef.Hex()) + } + + entries = append(entries, &ledgerv1.JournalEntryResponse{ + EntryRef: entryRefHex, + IdempotencyKey: entry.IdempotencyKey, + EntryType: modelEntryTypeToProto(entry.EntryType), + Description: entry.Description, + EventTime: timestamppb.New(entry.EventTime), + Version: entry.Version, + Lines: protoLines, + Metadata: entry.Metadata, + LedgerAccountRefs: accountRefs, + }) + } + + // Generate next cursor + nextCursor := "" + if hasMore { + nextCursor = encodeCursor(offset + limit) + } + + return &ledgerv1.StatementResponse{ + Entries: entries, + NextCursor: nextCursor, + }, nil + } +} + +// parseCursor decodes a pagination cursor +func parseCursor(cursor string) (int, error) { + decoded, err := base64.StdEncoding.DecodeString(cursor) + if err != nil { + return 0, fmt.Errorf("invalid base64: %w", err) + } + parts := strings.Split(string(decoded), ":") + if len(parts) != 2 || parts[0] != "offset" { + return 0, fmt.Errorf("invalid cursor format") + } + offset, err := strconv.Atoi(parts[1]) + if err != nil { + return 0, fmt.Errorf("invalid offset: %w", err) + } + return offset, nil +} + +// encodeCursor encodes an offset into a pagination cursor +func encodeCursor(offset int) string { + cursor := fmt.Sprintf("offset:%d", offset) + return base64.StdEncoding.EncodeToString([]byte(cursor)) +} diff --git a/api/ledger/internal/service/ledger/queries_test.go b/api/ledger/internal/service/ledger/queries_test.go new file mode 100644 index 0000000..fdbc6ca --- /dev/null +++ b/api/ledger/internal/service/ledger/queries_test.go @@ -0,0 +1,99 @@ +package ledger + +import ( + "testing" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +func TestParseCursor(t *testing.T) { + t.Run("ValidCursor", func(t *testing.T) { + cursor := encodeCursor(100) + offset, err := parseCursor(cursor) + + require.NoError(t, err) + assert.Equal(t, 100, offset) + }) + + t.Run("ZeroOffset", func(t *testing.T) { + cursor := encodeCursor(0) + offset, err := parseCursor(cursor) + + require.NoError(t, err) + assert.Equal(t, 0, offset) + }) + + t.Run("InvalidBase64", func(t *testing.T) { + _, err := parseCursor("not-valid-base64!!!") + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid base64") + }) + + t.Run("InvalidFormat", func(t *testing.T) { + // Encode something that's not in the expected format + invalidCursor := "aW52YWxpZC1mb3JtYXQ=" // base64 of "invalid-format" + _, err := parseCursor(invalidCursor) + + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid cursor format") + }) + + t.Run("InvalidOffsetValue", func(t *testing.T) { + // Create a cursor with non-numeric offset + invalidCursor := "b2Zmc2V0OmFiYw==" // base64 of "offset:abc" + _, err := parseCursor(invalidCursor) + + require.Error(t, err) + assert.Contains(t, err.Error(), "invalid offset") + }) + + t.Run("NegativeOffset", func(t *testing.T) { + cursor := encodeCursor(-10) + offset, err := parseCursor(cursor) + + require.NoError(t, err) + assert.Equal(t, -10, offset) + }) +} + +func TestEncodeCursor(t *testing.T) { + t.Run("PositiveOffset", func(t *testing.T) { + cursor := encodeCursor(100) + assert.NotEmpty(t, cursor) + + // Verify it can be parsed back + offset, err := parseCursor(cursor) + require.NoError(t, err) + assert.Equal(t, 100, offset) + }) + + t.Run("ZeroOffset", func(t *testing.T) { + cursor := encodeCursor(0) + assert.NotEmpty(t, cursor) + + offset, err := parseCursor(cursor) + require.NoError(t, err) + assert.Equal(t, 0, offset) + }) + + t.Run("LargeOffset", func(t *testing.T) { + cursor := encodeCursor(999999) + assert.NotEmpty(t, cursor) + + offset, err := parseCursor(cursor) + require.NoError(t, err) + assert.Equal(t, 999999, offset) + }) + + t.Run("RoundTrip", func(t *testing.T) { + testOffsets := []int{0, 1, 10, 50, 100, 500, 1000, 10000} + + for _, expected := range testOffsets { + cursor := encodeCursor(expected) + actual, err := parseCursor(cursor) + require.NoError(t, err) + assert.Equal(t, expected, actual) + } + }) +} diff --git a/api/ledger/internal/service/ledger/service.go b/api/ledger/internal/service/ledger/service.go new file mode 100644 index 0000000..b5e95b6 --- /dev/null +++ b/api/ledger/internal/service/ledger/service.go @@ -0,0 +1,357 @@ +package ledger + +import ( + "context" + "fmt" + "strings" + "sync" + "time" + + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + "github.com/shopspring/decimal" + "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" + + accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + tracev1 "github.com/tech/sendico/pkg/proto/common/trace/v1" + + ledgerv1 "github.com/tech/sendico/ledger/internal/generated/service/ledger/v1" + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/pkg/api/routers" + pmessaging "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" +) + +type serviceError string + +func (e serviceError) Error() string { + return string(e) +} + +var ( + errStorageNotInitialized = serviceError("ledger: storage not initialized") +) + +type Service struct { + logger mlogger.Logger + storage storage.Repository + producer pmessaging.Producer + fees feesDependency + + outbox struct { + once sync.Once + cancel context.CancelFunc + publisher *outboxPublisher + } + ledgerv1.UnimplementedLedgerServiceServer +} + +type feesDependency struct { + client feesv1.FeeEngineClient + timeout time.Duration +} + +func (f feesDependency) available() bool { + return f.client != nil +} + +func NewService(logger mlogger.Logger, repo storage.Repository, prod pmessaging.Producer, feesClient feesv1.FeeEngineClient, feesTimeout time.Duration) *Service { + // Initialize Prometheus metrics + initMetrics() + + service := &Service{ + logger: logger.Named("ledger"), + storage: repo, + producer: prod, + fees: feesDependency{ + client: feesClient, + timeout: feesTimeout, + }, + } + + service.startOutboxPublisher() + return service +} + +func (s *Service) Register(router routers.GRPC) error { + return router.Register(func(reg grpc.ServiceRegistrar) { + ledgerv1.RegisterLedgerServiceServer(reg, s) + }) +} + +// CreateAccount provisions a new ledger account scoped to an organization. +func (s *Service) CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) { + responder := s.createAccountResponder(ctx, req) + return responder(ctx) +} + +// PostCreditWithCharges handles credit posting with fees in one atomic journal entry +func (s *Service) PostCreditWithCharges(ctx context.Context, req *ledgerv1.PostCreditRequest) (*ledgerv1.PostResponse, error) { + start := time.Now() + defer func() { + recordJournalEntry("credit", "attempted", time.Since(start).Seconds()) + }() + + responder := s.postCreditResponder(ctx, req) + resp, err := responder(ctx) + + if err != nil { + recordJournalEntryError("credit", "not_implemented") + } + + return resp, err +} + +// PostDebitWithCharges handles debit posting with fees in one atomic journal entry +func (s *Service) PostDebitWithCharges(ctx context.Context, req *ledgerv1.PostDebitRequest) (*ledgerv1.PostResponse, error) { + start := time.Now() + defer func() { + recordJournalEntry("debit", "attempted", time.Since(start).Seconds()) + }() + + responder := s.postDebitResponder(ctx, req) + resp, err := responder(ctx) + + if err != nil { + recordJournalEntryError("debit", "failed") + } + + return resp, err +} + +// TransferInternal handles internal transfer between accounts +func (s *Service) TransferInternal(ctx context.Context, req *ledgerv1.TransferRequest) (*ledgerv1.PostResponse, error) { + start := time.Now() + defer func() { + recordJournalEntry("transfer", "attempted", time.Since(start).Seconds()) + }() + + responder := s.transferResponder(ctx, req) + resp, err := responder(ctx) + + if err != nil { + recordJournalEntryError("transfer", "failed") + } + + return resp, err +} + +// ApplyFXWithCharges handles foreign exchange transaction with charges +func (s *Service) ApplyFXWithCharges(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) { + start := time.Now() + defer func() { + recordJournalEntry("fx", "attempted", time.Since(start).Seconds()) + }() + + responder := s.fxResponder(ctx, req) + resp, err := responder(ctx) + + if err != nil { + recordJournalEntryError("fx", "failed") + } + + return resp, err +} + +// GetBalance queries current account balance +func (s *Service) GetBalance(ctx context.Context, req *ledgerv1.GetBalanceRequest) (*ledgerv1.BalanceResponse, error) { + start := time.Now() + defer func() { + recordBalanceQuery("attempted", time.Since(start).Seconds()) + }() + + responder := s.getBalanceResponder(ctx, req) + resp, err := responder(ctx) + + return resp, err +} + +// GetJournalEntry gets journal entry details +func (s *Service) GetJournalEntry(ctx context.Context, req *ledgerv1.GetEntryRequest) (*ledgerv1.JournalEntryResponse, error) { + responder := s.getJournalEntryResponder(ctx, req) + return responder(ctx) +} + +func (s *Service) Shutdown() { + if s == nil { + return + } + if s.outbox.cancel != nil { + s.outbox.cancel() + } +} + +func (s *Service) startOutboxPublisher() { + if s.storage == nil || s.producer == nil { + return + } + + s.outbox.once.Do(func() { + outboxStore := s.storage.Outbox() + if outboxStore == nil { + return + } + + ctx, cancel := context.WithCancel(context.Background()) + s.outbox.cancel = cancel + s.outbox.publisher = newOutboxPublisher(s.logger, outboxStore, s.producer) + + go s.outbox.publisher.run(ctx) + }) +} + +// GetStatement gets account statement with pagination +func (s *Service) GetStatement(ctx context.Context, req *ledgerv1.GetStatementRequest) (*ledgerv1.StatementResponse, error) { + responder := s.getStatementResponder(ctx, req) + return responder(ctx) +} + +func (s *Service) pingStorage(ctx context.Context) error { + if s.storage == nil { + return errStorageNotInitialized + } + return s.storage.Ping(ctx) +} + +func (s *Service) quoteFeesForCredit(ctx context.Context, req *ledgerv1.PostCreditRequest) ([]*ledgerv1.PostingLine, error) { + if !s.fees.available() { + return nil, nil + } + attrs := map[string]string{} + if strings.TrimSpace(req.GetDescription()) != "" { + attrs["description"] = req.GetDescription() + } + return s.quoteFees(ctx, feesv1.Trigger_TRIGGER_CAPTURE, req.GetOrganizationRef(), req.GetIdempotencyKey(), req.GetLedgerAccountRef(), "ledger.post_credit", req.GetIdempotencyKey(), req.GetEventTime(), req.Money, attrs) +} + +func (s *Service) quoteFeesForDebit(ctx context.Context, req *ledgerv1.PostDebitRequest) ([]*ledgerv1.PostingLine, error) { + if !s.fees.available() { + return nil, nil + } + attrs := map[string]string{} + if strings.TrimSpace(req.GetDescription()) != "" { + attrs["description"] = req.GetDescription() + } + return s.quoteFees(ctx, feesv1.Trigger_TRIGGER_REFUND, req.GetOrganizationRef(), req.GetIdempotencyKey(), req.GetLedgerAccountRef(), "ledger.post_debit", req.GetIdempotencyKey(), req.GetEventTime(), req.Money, attrs) +} + +func (s *Service) quoteFees(ctx context.Context, trigger feesv1.Trigger, organizationRef, idempotencyKey, ledgerAccountRef, originType, originRef string, eventTime *timestamppb.Timestamp, baseAmount *moneyv1.Money, attributes map[string]string) ([]*ledgerv1.PostingLine, error) { + if !s.fees.available() { + return nil, nil + } + if strings.TrimSpace(organizationRef) == "" { + return nil, fmt.Errorf("organization reference is required to quote fees") + } + if baseAmount == nil { + return nil, fmt.Errorf("base amount is required to quote fees") + } + + amountCopy := &moneyv1.Money{Amount: baseAmount.GetAmount(), Currency: baseAmount.GetCurrency()} + bookedAt := eventTime + if bookedAt == nil { + bookedAt = timestamppb.Now() + } + + trace := &tracev1.TraceContext{ + RequestRef: idempotencyKey, + IdempotencyKey: idempotencyKey, + } + + req := &feesv1.QuoteFeesRequest{ + Meta: &feesv1.RequestMeta{ + OrganizationRef: organizationRef, + Trace: trace, + }, + Intent: &feesv1.Intent{ + Trigger: trigger, + BaseAmount: amountCopy, + BookedAt: bookedAt, + OriginType: originType, + OriginRef: originRef, + Attributes: map[string]string{}, + }, + } + + if ledgerAccountRef != "" { + req.Intent.Attributes["ledger_account_ref"] = ledgerAccountRef + } + for k, v := range attributes { + if strings.TrimSpace(k) == "" { + continue + } + req.Intent.Attributes[k] = v + } + + callCtx := ctx + if s.fees.timeout > 0 { + var cancel context.CancelFunc + callCtx, cancel = context.WithTimeout(ctx, s.fees.timeout) + defer cancel() + } + + resp, err := s.fees.client.QuoteFees(callCtx, req) + if err != nil { + return nil, err + } + + lines, err := convertFeeDerivedLines(resp.GetLines()) + if err != nil { + return nil, err + } + return lines, nil +} + +func convertFeeDerivedLines(lines []*feesv1.DerivedPostingLine) ([]*ledgerv1.PostingLine, error) { + result := make([]*ledgerv1.PostingLine, 0, len(lines)) + for idx, line := range lines { + if line == nil { + continue + } + if line.GetMoney() == nil { + return nil, fmt.Errorf("fee line %d missing money", idx) + } + dec, err := decimal.NewFromString(line.GetMoney().GetAmount()) + if err != nil { + return nil, fmt.Errorf("fee line %d invalid amount: %w", idx, err) + } + dec = ensureAmountForSide(dec, line.GetSide()) + posting := &ledgerv1.PostingLine{ + LedgerAccountRef: line.GetLedgerAccountRef(), + Money: &moneyv1.Money{ + Amount: dec.String(), + Currency: line.GetMoney().GetCurrency(), + }, + LineType: mapFeeLineType(line.GetLineType()), + } + result = append(result, posting) + } + return result, nil +} + +func ensureAmountForSide(amount decimal.Decimal, side accountingv1.EntrySide) decimal.Decimal { + switch side { + case accountingv1.EntrySide_ENTRY_SIDE_DEBIT: + if amount.Sign() > 0 { + return amount.Neg() + } + case accountingv1.EntrySide_ENTRY_SIDE_CREDIT: + if amount.Sign() < 0 { + return amount.Neg() + } + } + return amount +} + +func mapFeeLineType(lineType accountingv1.PostingLineType) ledgerv1.LineType { + switch lineType { + case accountingv1.PostingLineType_POSTING_LINE_FEE: + return ledgerv1.LineType_LINE_FEE + case accountingv1.PostingLineType_POSTING_LINE_SPREAD: + return ledgerv1.LineType_LINE_SPREAD + case accountingv1.PostingLineType_POSTING_LINE_REVERSAL: + return ledgerv1.LineType_LINE_REVERSAL + default: + return ledgerv1.LineType_LINE_FEE + } +} diff --git a/api/ledger/main.go b/api/ledger/main.go new file mode 100644 index 0000000..73f49e3 --- /dev/null +++ b/api/ledger/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/tech/sendico/ledger/internal/appversion" + si "github.com/tech/sendico/ledger/internal/server" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" + smain "github.com/tech/sendico/pkg/server/main" +) + +func factory(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return si.Create(logger, file, debug) +} + +func main() { + smain.RunServer("main", appversion.Create(), factory) +} diff --git a/api/ledger/storage/model/account.go b/api/ledger/storage/model/account.go new file mode 100644 index 0000000..27a72f1 --- /dev/null +++ b/api/ledger/storage/model/account.go @@ -0,0 +1,25 @@ +package model + +import ( + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" +) + +// Account represents a ledger account that holds balances for a specific currency. +type Account struct { + storable.Base `bson:",inline" json:",inline"` + model.PermissionBound `bson:",inline" json:",inline"` + + AccountCode string `bson:"accountCode" json:"accountCode"` // e.g., "asset:cash:usd" + Currency string `bson:"currency" json:"currency"` // ISO 4217 currency code + AccountType AccountType `bson:"accountType" json:"accountType"` // asset, liability, revenue, expense + Status AccountStatus `bson:"status" json:"status"` // active, frozen, closed + AllowNegative bool `bson:"allowNegative" json:"allowNegative"` // debit policy: allow negative balances + IsSettlement bool `bson:"isSettlement,omitempty" json:"isSettlement,omitempty"` // marks org-level default contra account + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// Collection implements storable.Storable. +func (*Account) Collection() string { + return AccountsCollection +} diff --git a/api/ledger/storage/model/account_balance.go b/api/ledger/storage/model/account_balance.go new file mode 100644 index 0000000..b0d8e25 --- /dev/null +++ b/api/ledger/storage/model/account_balance.go @@ -0,0 +1,27 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// AccountBalance represents the current balance of a ledger account. +// This is a materialized view updated atomically with journal entries. +type AccountBalance struct { + storable.Base `bson:",inline" json:",inline"` + model.PermissionBound `bson:",inline" json:",inline"` + + AccountRef primitive.ObjectID `bson:"accountRef" json:"accountRef"` // unique per account+currency + Balance string `bson:"balance" json:"balance"` // stored as string for exact decimal + Currency string `bson:"currency" json:"currency"` // ISO 4217 currency code + Version int64 `bson:"version" json:"version"` // for optimistic locking + LastUpdated time.Time `bson:"lastUpdated" json:"lastUpdated"` // timestamp of last balance update +} + +// Collection implements storable.Storable. +func (*AccountBalance) Collection() string { + return AccountBalancesCollection +} diff --git a/api/ledger/storage/model/journal_entry.go b/api/ledger/storage/model/journal_entry.go new file mode 100644 index 0000000..5bc237f --- /dev/null +++ b/api/ledger/storage/model/journal_entry.go @@ -0,0 +1,26 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" +) + +// JournalEntry represents an atomic ledger transaction with multiple posting lines. +type JournalEntry struct { + storable.Base `bson:",inline" json:",inline"` + model.PermissionBound `bson:",inline" json:",inline"` + + IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"` // unique key for deduplication + EventTime time.Time `bson:"eventTime" json:"eventTime"` // business event timestamp + EntryType EntryType `bson:"entryType" json:"entryType"` // credit, debit, transfer, fx, fee, adjust, reverse + Description string `bson:"description" json:"description"` + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` + Version int64 `bson:"version" json:"version"` // for ordering and optimistic locking +} + +// Collection implements storable.Storable. +func (*JournalEntry) Collection() string { + return JournalEntriesCollection +} diff --git a/api/ledger/storage/model/outbox.go b/api/ledger/storage/model/outbox.go new file mode 100644 index 0000000..e37fb4d --- /dev/null +++ b/api/ledger/storage/model/outbox.go @@ -0,0 +1,27 @@ +package model + +import ( + "time" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" +) + +// OutboxEvent represents a pending event to be published to NATS. +// Part of the transactional outbox pattern for reliable event delivery. +type OutboxEvent struct { + storable.Base `bson:",inline" json:",inline"` + model.OrganizationBoundBase `bson:",inline" json:",inline"` + + EventID string `bson:"eventId" json:"eventId"` // deterministic ID for NATS Msg-Id deduplication + Subject string `bson:"subject" json:"subject"` // NATS subject to publish to + Payload []byte `bson:"payload" json:"payload"` // JSON-encoded event data + Status OutboxStatus `bson:"status" json:"status"` // pending, sent, failed + Attempts int `bson:"attempts" json:"attempts"` // number of delivery attempts + SentAt *time.Time `bson:"sentAt,omitempty" json:"sentAt,omitempty"` +} + +// Collection implements storable.Storable. +func (*OutboxEvent) Collection() string { + return OutboxCollection +} diff --git a/api/ledger/storage/model/posting_line.go b/api/ledger/storage/model/posting_line.go new file mode 100644 index 0000000..209c6da --- /dev/null +++ b/api/ledger/storage/model/posting_line.go @@ -0,0 +1,24 @@ +package model + +import ( + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PostingLine represents a single debit or credit line in a journal entry. +type PostingLine struct { + storable.Base `bson:",inline" json:",inline"` + model.PermissionBound `bson:",inline" json:",inline"` + + JournalEntryRef primitive.ObjectID `bson:"journalEntryRef" json:"journalEntryRef"` + AccountRef primitive.ObjectID `bson:"accountRef" json:"accountRef"` + Amount string `bson:"amount" json:"amount"` // stored as string for exact decimal, positive = credit, negative = debit + Currency string `bson:"currency" json:"currency"` // ISO 4217 currency code + LineType LineType `bson:"lineType" json:"lineType"` // main, fee, spread, reversal +} + +// Collection implements storable.Storable. +func (*PostingLine) Collection() string { + return PostingLinesCollection +} diff --git a/api/ledger/storage/model/types.go b/api/ledger/storage/model/types.go new file mode 100644 index 0000000..653f004 --- /dev/null +++ b/api/ledger/storage/model/types.go @@ -0,0 +1,78 @@ +package model + +import "github.com/tech/sendico/pkg/model" + +// Collection names used by the ledger persistence layer. +const ( + AccountsCollection = "ledger_accounts" + JournalEntriesCollection = "journal_entries" + PostingLinesCollection = "posting_lines" + AccountBalancesCollection = "account_balances" + OutboxCollection = "outbox" +) + +// AccountType defines the category of account (asset, liability, revenue, expense). +type AccountType string + +const ( + AccountTypeAsset AccountType = "asset" + AccountTypeLiability AccountType = "liability" + AccountTypeRevenue AccountType = "revenue" + AccountTypeExpense AccountType = "expense" +) + +// AccountStatus tracks the operational state of an account. +type AccountStatus string + +const ( + AccountStatusActive AccountStatus = "active" + AccountStatusFrozen AccountStatus = "frozen" + AccountStatusClosed AccountStatus = "closed" +) + +// EntryType categorizes journal entries by their business purpose. +type EntryType string + +const ( + EntryTypeCredit EntryType = "credit" + EntryTypeDebit EntryType = "debit" + EntryTypeTransfer EntryType = "transfer" + EntryTypeFX EntryType = "fx" + EntryTypeFee EntryType = "fee" + EntryTypeAdjust EntryType = "adjust" + EntryTypeReverse EntryType = "reverse" +) + +// LineType distinguishes the role of a posting line within a journal entry. +type LineType string + +const ( + LineTypeMain LineType = "main" + LineTypeFee LineType = "fee" + LineTypeSpread LineType = "spread" + LineTypeReversal LineType = "reversal" +) + +// OutboxStatus tracks the delivery state of an outbox event. +type OutboxStatus string + +const ( + OutboxStatusPending OutboxStatus = "pending" + OutboxStatusSent OutboxStatus = "sent" + OutboxStatusFailed OutboxStatus = "failed" +) + +// Money represents an exact decimal amount with its currency. +type Money struct { + Currency string `bson:"currency" json:"currency"` + Amount string `bson:"amount" json:"amount"` // stored as string for exact decimal representation +} + +// LedgerMeta carries organization-scoped metadata for ledger entities. +type LedgerMeta struct { + model.OrganizationBoundBase `bson:",inline" json:",inline"` + + RequestRef string `bson:"requestRef,omitempty" json:"requestRef,omitempty"` + TraceRef string `bson:"traceRef,omitempty" json:"traceRef,omitempty"` + IdempotencyKey string `bson:"idempotencyKey,omitempty" json:"idempotencyKey,omitempty"` +} diff --git a/api/ledger/storage/mongo/repository.go b/api/ledger/storage/mongo/repository.go new file mode 100644 index 0000000..d4521d0 --- /dev/null +++ b/api/ledger/storage/mongo/repository.go @@ -0,0 +1,132 @@ +package mongo + +import ( + "context" + "time" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/mongo/store" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/db/transaction" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type Store struct { + logger mlogger.Logger + conn *db.MongoConnection + db *mongo.Database + txFactory transaction.Factory + + accounts storage.AccountsStore + journalEntries storage.JournalEntriesStore + postingLines storage.PostingLinesStore + balances storage.BalancesStore + outbox storage.OutboxStore +} + +func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) { + if conn == nil { + return nil, merrors.InvalidArgument("mongo connection is nil") + } + + client := conn.Client() + if client == nil { + return nil, merrors.Internal("mongo client not initialised") + } + + db := conn.Database() + txFactory := newMongoTransactionFactory(client) + + s := &Store{ + logger: logger.Named("storage").Named("mongo"), + conn: conn, + db: db, + txFactory: txFactory, + } + + ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second) + defer cancel() + + if err := s.Ping(ctx); err != nil { + s.logger.Error("mongo ping failed during store init", zap.Error(err)) + return nil, err + } + + // Initialize stores + accountsStore, err := store.NewAccounts(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize accounts store", zap.Error(err)) + return nil, err + } + + journalEntriesStore, err := store.NewJournalEntries(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize journal entries store", zap.Error(err)) + return nil, err + } + + postingLinesStore, err := store.NewPostingLines(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize posting lines store", zap.Error(err)) + return nil, err + } + + balancesStore, err := store.NewBalances(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize balances store", zap.Error(err)) + return nil, err + } + + outboxStore, err := store.NewOutbox(s.logger, db) + if err != nil { + s.logger.Error("failed to initialize outbox store", zap.Error(err)) + return nil, err + } + + s.accounts = accountsStore + s.journalEntries = journalEntriesStore + s.postingLines = postingLinesStore + s.balances = balancesStore + s.outbox = outboxStore + + s.logger.Info("Ledger MongoDB storage initialized") + + return s, nil +} + +func (s *Store) Ping(ctx context.Context) error { + return s.conn.Ping(ctx) +} + +func (s *Store) Accounts() storage.AccountsStore { + return s.accounts +} + +func (s *Store) JournalEntries() storage.JournalEntriesStore { + return s.journalEntries +} + +func (s *Store) PostingLines() storage.PostingLinesStore { + return s.postingLines +} + +func (s *Store) Balances() storage.BalancesStore { + return s.balances +} + +func (s *Store) Outbox() storage.OutboxStore { + return s.outbox +} + +func (s *Store) Database() *mongo.Database { + return s.db +} + +func (s *Store) TransactionFactory() transaction.Factory { + return s.txFactory +} + +var _ storage.Repository = (*Store)(nil) diff --git a/api/ledger/storage/mongo/store/accounts.go b/api/ledger/storage/mongo/store/accounts.go new file mode 100644 index 0000000..b215a51 --- /dev/null +++ b/api/ledger/storage/mongo/store/accounts.go @@ -0,0 +1,220 @@ +package store + +import ( + "context" + "errors" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type accountsStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewAccounts(logger mlogger.Logger, db *mongo.Database) (storage.AccountsStore, error) { + repo := repository.CreateMongoRepository(db, model.AccountsCollection) + + // Create compound index on organizationRef + accountCode + currency (unique) + uniqueIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "organizationRef", Sort: ri.Asc}, + {Field: "accountCode", Sort: ri.Asc}, + {Field: "currency", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(uniqueIndex); err != nil { + logger.Error("failed to ensure accounts unique index", zap.Error(err)) + return nil, err + } + + // Create index on organizationRef for listing + orgIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "organizationRef", Sort: ri.Asc}, + }, + } + if err := repo.CreateIndex(orgIndex); err != nil { + logger.Error("failed to ensure accounts organization index", zap.Error(err)) + return nil, err + } + + childLogger := logger.Named(model.AccountsCollection) + childLogger.Debug("accounts store initialised", zap.String("collection", model.AccountsCollection)) + + return &accountsStore{ + logger: childLogger, + repo: repo, + }, nil +} + +func (a *accountsStore) Create(ctx context.Context, account *model.Account) error { + if account == nil { + a.logger.Warn("attempt to create nil account") + return merrors.InvalidArgument("accountsStore: nil account") + } + + if err := a.repo.Insert(ctx, account, nil); err != nil { + if mongo.IsDuplicateKeyError(err) { + a.logger.Warn("duplicate account code", zap.String("accountCode", account.AccountCode), + zap.String("currency", account.Currency)) + return merrors.DataConflict("account with this code and currency already exists") + } + a.logger.Warn("failed to create account", zap.Error(err)) + return err + } + + a.logger.Debug("account created", zap.String("accountCode", account.AccountCode), + zap.String("currency", account.Currency)) + return nil +} + +func (a *accountsStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.Account, error) { + if accountRef.IsZero() { + a.logger.Warn("attempt to get account with zero ID") + return nil, merrors.InvalidArgument("accountsStore: zero account ID") + } + + result := &model.Account{} + if err := a.repo.Get(ctx, accountRef, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + a.logger.Debug("account not found", zap.String("accountRef", accountRef.Hex())) + return nil, storage.ErrAccountNotFound + } + a.logger.Warn("failed to get account", zap.Error(err), zap.String("accountRef", accountRef.Hex())) + return nil, err + } + + a.logger.Debug("account loaded", zap.String("accountRef", accountRef.Hex()), + zap.String("accountCode", result.AccountCode)) + return result, nil +} + +func (a *accountsStore) GetByAccountCode(ctx context.Context, orgRef primitive.ObjectID, accountCode, currency string) (*model.Account, error) { + if orgRef.IsZero() { + a.logger.Warn("attempt to get account with zero organization ID") + return nil, merrors.InvalidArgument("accountsStore: zero organization ID") + } + if accountCode == "" { + a.logger.Warn("attempt to get account with empty code") + return nil, merrors.InvalidArgument("accountsStore: empty account code") + } + if currency == "" { + a.logger.Warn("attempt to get account with empty currency") + return nil, merrors.InvalidArgument("accountsStore: empty currency") + } + + query := repository.Query(). + Filter(repository.Field("organizationRef"), orgRef). + Filter(repository.Field("accountCode"), accountCode). + Filter(repository.Field("currency"), currency) + + result := &model.Account{} + if err := a.repo.FindOneByFilter(ctx, query, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + a.logger.Debug("account not found by code", zap.String("accountCode", accountCode), + zap.String("currency", currency)) + return nil, storage.ErrAccountNotFound + } + a.logger.Warn("failed to get account by code", zap.Error(err), zap.String("accountCode", accountCode)) + return nil, err + } + + a.logger.Debug("account loaded by code", zap.String("accountCode", accountCode), + zap.String("currency", currency)) + return result, nil +} + +func (a *accountsStore) GetDefaultSettlement(ctx context.Context, orgRef primitive.ObjectID, currency string) (*model.Account, error) { + if orgRef.IsZero() { + a.logger.Warn("attempt to get default settlement with zero organization ID") + return nil, merrors.InvalidArgument("accountsStore: zero organization ID") + } + if currency == "" { + a.logger.Warn("attempt to get default settlement with empty currency") + return nil, merrors.InvalidArgument("accountsStore: empty currency") + } + + limit := int64(1) + query := repository.Query(). + Filter(repository.Field("organizationRef"), orgRef). + Filter(repository.Field("currency"), currency). + Filter(repository.Field("isSettlement"), true). + Limit(&limit) + + result := &model.Account{} + if err := a.repo.FindOneByFilter(ctx, query, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + a.logger.Debug("default settlement account not found", + zap.String("currency", currency), + zap.String("organizationRef", orgRef.Hex())) + return nil, storage.ErrAccountNotFound + } + a.logger.Warn("failed to get default settlement account", zap.Error(err), + zap.String("organizationRef", orgRef.Hex()), + zap.String("currency", currency)) + return nil, err + } + + a.logger.Debug("default settlement account loaded", + zap.String("accountRef", result.GetID().Hex()), + zap.String("currency", currency)) + return result, nil +} + +func (a *accountsStore) ListByOrganization(ctx context.Context, orgRef primitive.ObjectID, limit int, offset int) ([]*model.Account, error) { + if orgRef.IsZero() { + a.logger.Warn("attempt to list accounts with zero organization ID") + return nil, merrors.InvalidArgument("accountsStore: zero organization ID") + } + + limit64 := int64(limit) + offset64 := int64(offset) + query := repository.Query(). + Filter(repository.Field("organizationRef"), orgRef). + Limit(&limit64). + Offset(&offset64) + + accounts := make([]*model.Account, 0) + err := a.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.Account{} + if err := cur.Decode(doc); err != nil { + return err + } + accounts = append(accounts, doc) + return nil + }) + if err != nil { + a.logger.Warn("failed to list accounts", zap.Error(err)) + return nil, err + } + + a.logger.Debug("listed accounts", zap.Int("count", len(accounts))) + return accounts, nil +} + +func (a *accountsStore) UpdateStatus(ctx context.Context, accountRef primitive.ObjectID, status model.AccountStatus) error { + if accountRef.IsZero() { + a.logger.Warn("attempt to update account status with zero ID") + return merrors.InvalidArgument("accountsStore: zero account ID") + } + + patch := repository.Patch().Set(repository.Field("status"), status) + if err := a.repo.Patch(ctx, accountRef, patch); err != nil { + a.logger.Warn("failed to update account status", zap.Error(err), zap.String("accountRef", accountRef.Hex())) + return err + } + + a.logger.Debug("account status updated", zap.String("accountRef", accountRef.Hex()), + zap.String("status", string(status))) + return nil +} diff --git a/api/ledger/storage/mongo/store/accounts_test.go b/api/ledger/storage/mongo/store/accounts_test.go new file mode 100644 index 0000000..11dffdf --- /dev/null +++ b/api/ledger/storage/mongo/store/accounts_test.go @@ -0,0 +1,436 @@ +package store + +import ( + "context" + "errors" + "testing" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +func TestAccountsStore_Create(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + var insertedAccount *model.Account + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + insertedAccount = object.(*model.Account) + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + account := &model.Account{ + AccountCode: "1000", + Currency: "USD", + AccountType: model.AccountTypeAsset, + Status: model.AccountStatusActive, + AllowNegative: false, + } + + err := store.Create(ctx, account) + + require.NoError(t, err) + assert.NotNil(t, insertedAccount) + assert.Equal(t, "1000", insertedAccount.AccountCode) + assert.Equal(t, "USD", insertedAccount.Currency) + }) + + t.Run("NilAccount", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + err := store.Create(ctx, nil) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("DuplicateAccountCode", func(t *testing.T) { + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return mongo.WriteException{ + WriteErrors: []mongo.WriteError{ + {Code: 11000}, // Duplicate key error + }, + } + }, + } + + store := &accountsStore{logger: logger, repo: stub} + account := &model.Account{ + AccountCode: "1000", + Currency: "USD", + } + + err := store.Create(ctx, account) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrDataConflict)) + }) + + t.Run("InsertError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return expectedErr + }, + } + + store := &accountsStore{logger: logger, repo: stub} + account := &model.Account{AccountCode: "1000", Currency: "USD"} + + err := store.Create(ctx, account) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestAccountsStore_Get(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + accountRef := primitive.NewObjectID() + stub := &repositoryStub{ + GetFunc: func(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + account := result.(*model.Account) + account.SetID(accountRef) + account.AccountCode = "1000" + account.Currency = "USD" + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.Get(ctx, accountRef) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "1000", result.AccountCode) + assert.Equal(t, "USD", result.Currency) + }) + + t.Run("ZeroID", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + result, err := store.Get(ctx, primitive.NilObjectID) + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("NotFound", func(t *testing.T) { + accountRef := primitive.NewObjectID() + stub := &repositoryStub{ + GetFunc: func(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + return merrors.ErrNoData + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.Get(ctx, accountRef) + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, storage.ErrAccountNotFound)) + }) + + t.Run("GetError", func(t *testing.T) { + accountRef := primitive.NewObjectID() + expectedErr := errors.New("database error") + stub := &repositoryStub{ + GetFunc: func(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + return expectedErr + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.Get(ctx, accountRef) + + require.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, expectedErr, err) + }) +} + +func TestAccountsStore_GetByAccountCode(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + orgRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + account := result.(*model.Account) + account.AccountCode = "1000" + account.Currency = "USD" + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.GetByAccountCode(ctx, orgRef, "1000", "USD") + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "1000", result.AccountCode) + assert.Equal(t, "USD", result.Currency) + }) + + t.Run("ZeroOrganizationID", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + result, err := store.GetByAccountCode(ctx, primitive.NilObjectID, "1000", "USD") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyAccountCode", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + result, err := store.GetByAccountCode(ctx, orgRef, "", "USD") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyCurrency", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + result, err := store.GetByAccountCode(ctx, orgRef, "1000", "") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("NotFound", func(t *testing.T) { + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return merrors.ErrNoData + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.GetByAccountCode(ctx, orgRef, "9999", "USD") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, storage.ErrAccountNotFound)) + }) +} + +func TestAccountsStore_GetDefaultSettlement(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + orgRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + account := result.(*model.Account) + account.SetID(primitive.NewObjectID()) + account.Currency = "USD" + account.IsSettlement = true + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.GetDefaultSettlement(ctx, orgRef, "USD") + + require.NoError(t, err) + assert.NotNil(t, result) + assert.True(t, result.IsSettlement) + assert.Equal(t, "USD", result.Currency) + }) + + t.Run("ZeroOrganizationID", func(t *testing.T) { + store := &accountsStore{logger: logger, repo: &repositoryStub{}} + result, err := store.GetDefaultSettlement(ctx, primitive.NilObjectID, "USD") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyCurrency", func(t *testing.T) { + store := &accountsStore{logger: logger, repo: &repositoryStub{}} + result, err := store.GetDefaultSettlement(ctx, orgRef, "") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("NotFound", func(t *testing.T) { + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return merrors.ErrNoData + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.GetDefaultSettlement(ctx, orgRef, "USD") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, storage.ErrAccountNotFound)) + }) + + t.Run("FindError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return expectedErr + }, + } + + store := &accountsStore{logger: logger, repo: stub} + result, err := store.GetDefaultSettlement(ctx, orgRef, "USD") + + require.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, expectedErr, err) + }) +} + +func TestAccountsStore_ListByOrganization(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + orgRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + var calledWithQuery bool + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + calledWithQuery = true + // In unit tests, we just verify the method is called correctly + // Integration tests would test the actual iteration logic + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 10, 0) + + require.NoError(t, err) + assert.True(t, calledWithQuery, "FindManyByFilter should have been called") + assert.NotNil(t, results) + }) + + t.Run("ZeroOrganizationID", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + results, err := store.ListByOrganization(ctx, primitive.NilObjectID, 10, 0) + + require.Error(t, err) + assert.Nil(t, results) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyResult", func(t *testing.T) { + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 10, 0) + + require.NoError(t, err) + assert.Len(t, results, 0) + }) + + t.Run("FindError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return expectedErr + }, + } + + store := &accountsStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 10, 0) + + require.Error(t, err) + assert.Nil(t, results) + assert.Equal(t, expectedErr, err) + }) +} + +func TestAccountsStore_UpdateStatus(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + accountRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + var patchedID primitive.ObjectID + var patchedStatus model.AccountStatus + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + patchedID = id + // In real test, we'd inspect patch builder but this is sufficient for stub + patchedStatus = model.AccountStatusFrozen + return nil + }, + } + + store := &accountsStore{logger: logger, repo: stub} + err := store.UpdateStatus(ctx, accountRef, model.AccountStatusFrozen) + + require.NoError(t, err) + assert.Equal(t, accountRef, patchedID) + assert.Equal(t, model.AccountStatusFrozen, patchedStatus) + }) + + t.Run("ZeroID", func(t *testing.T) { + stub := &repositoryStub{} + store := &accountsStore{logger: logger, repo: stub} + + err := store.UpdateStatus(ctx, primitive.NilObjectID, model.AccountStatusFrozen) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("PatchError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + return expectedErr + }, + } + + store := &accountsStore{logger: logger, repo: stub} + err := store.UpdateStatus(ctx, accountRef, model.AccountStatusFrozen) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} diff --git a/api/ledger/storage/mongo/store/balances.go b/api/ledger/storage/mongo/store/balances.go new file mode 100644 index 0000000..5c5c428 --- /dev/null +++ b/api/ledger/storage/mongo/store/balances.go @@ -0,0 +1,115 @@ +package store + +import ( + "context" + "errors" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type balancesStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewBalances(logger mlogger.Logger, db *mongo.Database) (storage.BalancesStore, error) { + repo := repository.CreateMongoRepository(db, model.AccountBalancesCollection) + + // Create unique index on accountRef (one balance per account) + uniqueIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "accountRef", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(uniqueIndex); err != nil { + logger.Error("failed to ensure balances unique index", zap.Error(err)) + return nil, err + } + + childLogger := logger.Named(model.AccountBalancesCollection) + childLogger.Debug("balances store initialised", zap.String("collection", model.AccountBalancesCollection)) + + return &balancesStore{ + logger: childLogger, + repo: repo, + }, nil +} + +func (b *balancesStore) Get(ctx context.Context, accountRef primitive.ObjectID) (*model.AccountBalance, error) { + if accountRef.IsZero() { + b.logger.Warn("attempt to get balance with zero account ID") + return nil, merrors.InvalidArgument("balancesStore: zero account ID") + } + + query := repository.Filter("accountRef", accountRef) + + result := &model.AccountBalance{} + if err := b.repo.FindOneByFilter(ctx, query, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + b.logger.Debug("balance not found", zap.String("accountRef", accountRef.Hex())) + return nil, storage.ErrBalanceNotFound + } + b.logger.Warn("failed to get balance", zap.Error(err), zap.String("accountRef", accountRef.Hex())) + return nil, err + } + + b.logger.Debug("balance loaded", zap.String("accountRef", accountRef.Hex()), + zap.String("balance", result.Balance)) + return result, nil +} + +func (b *balancesStore) Upsert(ctx context.Context, balance *model.AccountBalance) error { + if balance == nil { + b.logger.Warn("attempt to upsert nil balance") + return merrors.InvalidArgument("balancesStore: nil balance") + } + if balance.AccountRef.IsZero() { + b.logger.Warn("attempt to upsert balance with zero account ID") + return merrors.InvalidArgument("balancesStore: zero account ID") + } + + existing := &model.AccountBalance{} + filter := repository.Filter("accountRef", balance.AccountRef) + + if err := b.repo.FindOneByFilter(ctx, filter, existing); err != nil { + if errors.Is(err, merrors.ErrNoData) { + b.logger.Debug("inserting new balance", zap.String("accountRef", balance.AccountRef.Hex())) + return b.repo.Insert(ctx, balance, filter) + } + b.logger.Warn("failed to fetch balance", zap.Error(err), zap.String("accountRef", balance.AccountRef.Hex())) + return err + } + + if existing.GetID() != nil { + balance.SetID(*existing.GetID()) + } + b.logger.Debug("updating balance", zap.String("accountRef", balance.AccountRef.Hex()), + zap.String("balance", balance.Balance)) + return b.repo.Update(ctx, balance) +} + +func (b *balancesStore) IncrementBalance(ctx context.Context, accountRef primitive.ObjectID, amount string) error { + if accountRef.IsZero() { + b.logger.Warn("attempt to increment balance with zero account ID") + return merrors.InvalidArgument("balancesStore: zero account ID") + } + + // Note: This implementation uses $inc on a string field, which won't work. + // In a real implementation, you'd need to: + // 1. Fetch the balance + // 2. Parse amount strings to decimal + // 3. Add them + // 4. Update with optimistic locking via version field + // For now, return not implemented to indicate this needs proper decimal handling + b.logger.Warn("IncrementBalance not fully implemented - requires decimal arithmetic") + return merrors.NotImplemented("IncrementBalance requires proper decimal handling") +} diff --git a/api/ledger/storage/mongo/store/balances_test.go b/api/ledger/storage/mongo/store/balances_test.go new file mode 100644 index 0000000..03eab4e --- /dev/null +++ b/api/ledger/storage/mongo/store/balances_test.go @@ -0,0 +1,285 @@ +package store + +import ( + "context" + "errors" + "testing" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestBalancesStore_Get(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + accountRef := primitive.NewObjectID() + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + balance := result.(*model.AccountBalance) + balance.AccountRef = accountRef + balance.Balance = "1500.50" + balance.Version = 10 + return nil + }, + } + + store := &balancesStore{logger: logger, repo: stub} + result, err := store.Get(ctx, accountRef) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, accountRef, result.AccountRef) + assert.Equal(t, "1500.50", result.Balance) + assert.Equal(t, int64(10), result.Version) + }) + + t.Run("ZeroAccountID", func(t *testing.T) { + stub := &repositoryStub{} + store := &balancesStore{logger: logger, repo: stub} + + result, err := store.Get(ctx, primitive.NilObjectID) + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("NotFound", func(t *testing.T) { + accountRef := primitive.NewObjectID() + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return merrors.ErrNoData + }, + } + + store := &balancesStore{logger: logger, repo: stub} + result, err := store.Get(ctx, accountRef) + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, storage.ErrBalanceNotFound)) + }) + + t.Run("FindError", func(t *testing.T) { + accountRef := primitive.NewObjectID() + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return expectedErr + }, + } + + store := &balancesStore{logger: logger, repo: stub} + result, err := store.Get(ctx, accountRef) + + require.Error(t, err) + assert.Nil(t, result) + assert.Equal(t, expectedErr, err) + }) +} + +func TestBalancesStore_Upsert(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Insert_NewBalance", func(t *testing.T) { + accountRef := primitive.NewObjectID() + var insertedBalance *model.AccountBalance + + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return merrors.ErrNoData // Balance doesn't exist + }, + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + insertedBalance = object.(*model.AccountBalance) + return nil + }, + } + + store := &balancesStore{logger: logger, repo: stub} + balance := &model.AccountBalance{ + AccountRef: accountRef, + Balance: "1000.00", + Version: 1, + } + + err := store.Upsert(ctx, balance) + + require.NoError(t, err) + assert.NotNil(t, insertedBalance) + assert.Equal(t, "1000.00", insertedBalance.Balance) + }) + + t.Run("Update_ExistingBalance", func(t *testing.T) { + accountRef := primitive.NewObjectID() + existingID := primitive.NewObjectID() + var updatedBalance *model.AccountBalance + + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + existing := result.(*model.AccountBalance) + existing.SetID(existingID) + existing.AccountRef = accountRef + existing.Balance = "500.00" + existing.Version = 5 + return nil + }, + UpdateFunc: func(ctx context.Context, object storable.Storable) error { + updatedBalance = object.(*model.AccountBalance) + return nil + }, + } + + store := &balancesStore{logger: logger, repo: stub} + balance := &model.AccountBalance{ + AccountRef: accountRef, + Balance: "1500.00", + Version: 6, + } + + err := store.Upsert(ctx, balance) + + require.NoError(t, err) + assert.NotNil(t, updatedBalance) + assert.Equal(t, existingID, *updatedBalance.GetID()) + assert.Equal(t, "1500.00", updatedBalance.Balance) + assert.Equal(t, int64(6), updatedBalance.Version) + }) + + t.Run("NilBalance", func(t *testing.T) { + stub := &repositoryStub{} + store := &balancesStore{logger: logger, repo: stub} + + err := store.Upsert(ctx, nil) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("ZeroAccountID", func(t *testing.T) { + stub := &repositoryStub{} + store := &balancesStore{logger: logger, repo: stub} + + balance := &model.AccountBalance{ + AccountRef: primitive.NilObjectID, + Balance: "100.00", + } + + err := store.Upsert(ctx, balance) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("FindError", func(t *testing.T) { + accountRef := primitive.NewObjectID() + expectedErr := errors.New("database error") + + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return expectedErr + }, + } + + store := &balancesStore{logger: logger, repo: stub} + balance := &model.AccountBalance{ + AccountRef: accountRef, + Balance: "100.00", + } + + err := store.Upsert(ctx, balance) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("InsertError", func(t *testing.T) { + accountRef := primitive.NewObjectID() + expectedErr := errors.New("insert error") + + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return merrors.ErrNoData // Balance doesn't exist + }, + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return expectedErr + }, + } + + store := &balancesStore{logger: logger, repo: stub} + balance := &model.AccountBalance{ + AccountRef: accountRef, + Balance: "100.00", + } + + err := store.Upsert(ctx, balance) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("UpdateError", func(t *testing.T) { + accountRef := primitive.NewObjectID() + existingID := primitive.NewObjectID() + expectedErr := errors.New("update error") + + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + existing := result.(*model.AccountBalance) + existing.SetID(existingID) + existing.AccountRef = accountRef + existing.Balance = "500.00" + return nil + }, + UpdateFunc: func(ctx context.Context, object storable.Storable) error { + return expectedErr + }, + } + + store := &balancesStore{logger: logger, repo: stub} + balance := &model.AccountBalance{ + AccountRef: accountRef, + Balance: "1500.00", + } + + err := store.Upsert(ctx, balance) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestBalancesStore_IncrementBalance(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("NotImplemented", func(t *testing.T) { + accountRef := primitive.NewObjectID() + stub := &repositoryStub{} + store := &balancesStore{logger: logger, repo: stub} + + err := store.IncrementBalance(ctx, accountRef, "100.00") + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrNotImplemented)) + }) + + t.Run("ZeroAccountID", func(t *testing.T) { + stub := &repositoryStub{} + store := &balancesStore{logger: logger, repo: stub} + + err := store.IncrementBalance(ctx, primitive.NilObjectID, "100.00") + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) +} diff --git a/api/ledger/storage/mongo/store/journal_entries.go b/api/ledger/storage/mongo/store/journal_entries.go new file mode 100644 index 0000000..8968ef2 --- /dev/null +++ b/api/ledger/storage/mongo/store/journal_entries.go @@ -0,0 +1,160 @@ +package store + +import ( + "context" + "errors" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type journalEntriesStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewJournalEntries(logger mlogger.Logger, db *mongo.Database) (storage.JournalEntriesStore, error) { + repo := repository.CreateMongoRepository(db, model.JournalEntriesCollection) + + // Create unique index on organizationRef + idempotencyKey + uniqueIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "organizationRef", Sort: ri.Asc}, + {Field: "idempotencyKey", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(uniqueIndex); err != nil { + logger.Error("failed to ensure journal entries idempotency index", zap.Error(err)) + return nil, err + } + + // Create index on organizationRef for listing + orgIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "organizationRef", Sort: ri.Asc}, + {Field: "createdAt", Sort: ri.Desc}, + }, + } + if err := repo.CreateIndex(orgIndex); err != nil { + logger.Error("failed to ensure journal entries organization index", zap.Error(err)) + return nil, err + } + + childLogger := logger.Named(model.JournalEntriesCollection) + childLogger.Debug("journal entries store initialised", zap.String("collection", model.JournalEntriesCollection)) + + return &journalEntriesStore{ + logger: childLogger, + repo: repo, + }, nil +} + +func (j *journalEntriesStore) Create(ctx context.Context, entry *model.JournalEntry) error { + if entry == nil { + j.logger.Warn("attempt to create nil journal entry") + return merrors.InvalidArgument("journalEntriesStore: nil journal entry") + } + + if err := j.repo.Insert(ctx, entry, nil); err != nil { + if mongo.IsDuplicateKeyError(err) { + j.logger.Warn("duplicate idempotency key", zap.String("idempotencyKey", entry.IdempotencyKey)) + return storage.ErrDuplicateIdempotency + } + j.logger.Warn("failed to create journal entry", zap.Error(err)) + return err + } + + j.logger.Debug("journal entry created", zap.String("idempotencyKey", entry.IdempotencyKey), + zap.String("entryType", string(entry.EntryType))) + return nil +} + +func (j *journalEntriesStore) Get(ctx context.Context, entryRef primitive.ObjectID) (*model.JournalEntry, error) { + if entryRef.IsZero() { + j.logger.Warn("attempt to get journal entry with zero ID") + return nil, merrors.InvalidArgument("journalEntriesStore: zero entry ID") + } + + result := &model.JournalEntry{} + if err := j.repo.Get(ctx, entryRef, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + j.logger.Debug("journal entry not found", zap.String("entryRef", entryRef.Hex())) + return nil, storage.ErrJournalEntryNotFound + } + j.logger.Warn("failed to get journal entry", zap.Error(err), zap.String("entryRef", entryRef.Hex())) + return nil, err + } + + j.logger.Debug("journal entry loaded", zap.String("entryRef", entryRef.Hex()), + zap.String("idempotencyKey", result.IdempotencyKey)) + return result, nil +} + +func (j *journalEntriesStore) GetByIdempotencyKey(ctx context.Context, orgRef primitive.ObjectID, idempotencyKey string) (*model.JournalEntry, error) { + if orgRef.IsZero() { + j.logger.Warn("attempt to get journal entry with zero organization ID") + return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID") + } + if idempotencyKey == "" { + j.logger.Warn("attempt to get journal entry with empty idempotency key") + return nil, merrors.InvalidArgument("journalEntriesStore: empty idempotency key") + } + + query := repository.Query(). + Filter(repository.Field("organizationRef"), orgRef). + Filter(repository.Field("idempotencyKey"), idempotencyKey) + + result := &model.JournalEntry{} + if err := j.repo.FindOneByFilter(ctx, query, result); err != nil { + if errors.Is(err, merrors.ErrNoData) { + j.logger.Debug("journal entry not found by idempotency key", zap.String("idempotencyKey", idempotencyKey)) + return nil, storage.ErrJournalEntryNotFound + } + j.logger.Warn("failed to get journal entry by idempotency key", zap.Error(err), + zap.String("idempotencyKey", idempotencyKey)) + return nil, err + } + + j.logger.Debug("journal entry loaded by idempotency key", zap.String("idempotencyKey", idempotencyKey)) + return result, nil +} + +func (j *journalEntriesStore) ListByOrganization(ctx context.Context, orgRef primitive.ObjectID, limit int, offset int) ([]*model.JournalEntry, error) { + if orgRef.IsZero() { + j.logger.Warn("attempt to list journal entries with zero organization ID") + return nil, merrors.InvalidArgument("journalEntriesStore: zero organization ID") + } + + limit64 := int64(limit) + offset64 := int64(offset) + query := repository.Query(). + Filter(repository.Field("organizationRef"), orgRef). + Limit(&limit64). + Offset(&offset64). + Sort(repository.Field("createdAt"), false) // false = descending + + entries := make([]*model.JournalEntry, 0) + err := j.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.JournalEntry{} + if err := cur.Decode(doc); err != nil { + return err + } + entries = append(entries, doc) + return nil + }) + if err != nil { + j.logger.Warn("failed to list journal entries", zap.Error(err)) + return nil, err + } + + j.logger.Debug("listed journal entries", zap.Int("count", len(entries))) + return entries, nil +} diff --git a/api/ledger/storage/mongo/store/journal_entries_test.go b/api/ledger/storage/mongo/store/journal_entries_test.go new file mode 100644 index 0000000..34b5318 --- /dev/null +++ b/api/ledger/storage/mongo/store/journal_entries_test.go @@ -0,0 +1,299 @@ +package store + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +func TestJournalEntriesStore_Create(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + var insertedEntry *model.JournalEntry + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + insertedEntry = object.(*model.JournalEntry) + return nil + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + entry := &model.JournalEntry{ + IdempotencyKey: "test-key-123", + EventTime: time.Now(), + EntryType: model.EntryTypeCredit, + Description: "Test invoice entry", + } + + err := store.Create(ctx, entry) + + require.NoError(t, err) + assert.NotNil(t, insertedEntry) + assert.Equal(t, "test-key-123", insertedEntry.IdempotencyKey) + assert.Equal(t, model.EntryTypeCredit, insertedEntry.EntryType) + }) + + t.Run("NilEntry", func(t *testing.T) { + stub := &repositoryStub{} + store := &journalEntriesStore{logger: logger, repo: stub} + + err := store.Create(ctx, nil) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("DuplicateIdempotencyKey", func(t *testing.T) { + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return mongo.WriteException{ + WriteErrors: []mongo.WriteError{ + {Code: 11000}, // Duplicate key error + }, + } + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + entry := &model.JournalEntry{ + IdempotencyKey: "duplicate-key", + EventTime: time.Now(), + } + + err := store.Create(ctx, entry) + + require.Error(t, err) + assert.True(t, errors.Is(err, storage.ErrDuplicateIdempotency)) + }) + + t.Run("InsertError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return expectedErr + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + entry := &model.JournalEntry{ + IdempotencyKey: "test-key", + EventTime: time.Now(), + } + + err := store.Create(ctx, entry) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestJournalEntriesStore_Get(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + entryRef := primitive.NewObjectID() + stub := &repositoryStub{ + GetFunc: func(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + entry := result.(*model.JournalEntry) + entry.SetID(entryRef) + entry.IdempotencyKey = "test-key-123" + entry.EntryType = model.EntryTypeDebit + return nil + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + result, err := store.Get(ctx, entryRef) + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "test-key-123", result.IdempotencyKey) + assert.Equal(t, model.EntryTypeDebit, result.EntryType) + }) + + t.Run("ZeroID", func(t *testing.T) { + stub := &repositoryStub{} + store := &journalEntriesStore{logger: logger, repo: stub} + + result, err := store.Get(ctx, primitive.NilObjectID) + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("NotFound", func(t *testing.T) { + entryRef := primitive.NewObjectID() + stub := &repositoryStub{ + GetFunc: func(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + return merrors.ErrNoData + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + result, err := store.Get(ctx, entryRef) + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, storage.ErrJournalEntryNotFound)) + }) +} + +func TestJournalEntriesStore_GetByIdempotencyKey(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + orgRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + entry := result.(*model.JournalEntry) + entry.IdempotencyKey = "unique-key-123" + entry.EntryType = model.EntryTypeReverse + return nil + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + result, err := store.GetByIdempotencyKey(ctx, orgRef, "unique-key-123") + + require.NoError(t, err) + assert.NotNil(t, result) + assert.Equal(t, "unique-key-123", result.IdempotencyKey) + assert.Equal(t, model.EntryTypeReverse, result.EntryType) + }) + + t.Run("ZeroOrganizationID", func(t *testing.T) { + stub := &repositoryStub{} + store := &journalEntriesStore{logger: logger, repo: stub} + + result, err := store.GetByIdempotencyKey(ctx, primitive.NilObjectID, "test-key") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyIdempotencyKey", func(t *testing.T) { + stub := &repositoryStub{} + store := &journalEntriesStore{logger: logger, repo: stub} + + result, err := store.GetByIdempotencyKey(ctx, orgRef, "") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("NotFound", func(t *testing.T) { + stub := &repositoryStub{ + FindOneByFilterFunc: func(ctx context.Context, _ builder.Query, result storable.Storable) error { + return merrors.ErrNoData + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + result, err := store.GetByIdempotencyKey(ctx, orgRef, "nonexistent-key") + + require.Error(t, err) + assert.Nil(t, result) + assert.True(t, errors.Is(err, storage.ErrJournalEntryNotFound)) + }) +} + +func TestJournalEntriesStore_ListByOrganization(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + orgRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 10, 0) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("ZeroOrganizationID", func(t *testing.T) { + stub := &repositoryStub{} + store := &journalEntriesStore{logger: logger, repo: stub} + + results, err := store.ListByOrganization(ctx, primitive.NilObjectID, 10, 0) + + require.Error(t, err) + assert.Nil(t, results) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyResult", func(t *testing.T) { + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return nil + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 10, 0) + + require.NoError(t, err) + assert.Len(t, results, 0) + }) + + t.Run("WithPagination", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 2, 1) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("FindError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return expectedErr + }, + } + + store := &journalEntriesStore{logger: logger, repo: stub} + results, err := store.ListByOrganization(ctx, orgRef, 10, 0) + + require.Error(t, err) + assert.Nil(t, results) + assert.Equal(t, expectedErr, err) + }) +} diff --git a/api/ledger/storage/mongo/store/outbox.go b/api/ledger/storage/mongo/store/outbox.go new file mode 100644 index 0000000..4fb9bea --- /dev/null +++ b/api/ledger/storage/mongo/store/outbox.go @@ -0,0 +1,155 @@ +package store + +import ( + "context" + "time" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type outboxStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewOutbox(logger mlogger.Logger, db *mongo.Database) (storage.OutboxStore, error) { + repo := repository.CreateMongoRepository(db, model.OutboxCollection) + + // Create index on status + createdAt for efficient pending query + statusIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "status", Sort: ri.Asc}, + {Field: "createdAt", Sort: ri.Asc}, + }, + } + if err := repo.CreateIndex(statusIndex); err != nil { + logger.Error("failed to ensure outbox status index", zap.Error(err)) + return nil, err + } + + // Create unique index on eventId for deduplication + eventIdIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "eventId", Sort: ri.Asc}, + }, + Unique: true, + } + if err := repo.CreateIndex(eventIdIndex); err != nil { + logger.Error("failed to ensure outbox eventId index", zap.Error(err)) + return nil, err + } + + childLogger := logger.Named(model.OutboxCollection) + childLogger.Debug("outbox store initialised", zap.String("collection", model.OutboxCollection)) + + return &outboxStore{ + logger: childLogger, + repo: repo, + }, nil +} + +func (o *outboxStore) Create(ctx context.Context, event *model.OutboxEvent) error { + if event == nil { + o.logger.Warn("attempt to create nil outbox event") + return merrors.InvalidArgument("outboxStore: nil outbox event") + } + + if err := o.repo.Insert(ctx, event, nil); err != nil { + if mongo.IsDuplicateKeyError(err) { + o.logger.Warn("duplicate event ID", zap.String("eventId", event.EventID)) + return merrors.DataConflict("outbox event with this ID already exists") + } + o.logger.Warn("failed to create outbox event", zap.Error(err)) + return err + } + + o.logger.Debug("outbox event created", zap.String("eventId", event.EventID), + zap.String("subject", event.Subject)) + return nil +} + +func (o *outboxStore) ListPending(ctx context.Context, limit int) ([]*model.OutboxEvent, error) { + limit64 := int64(limit) + query := repository.Query(). + Filter(repository.Field("status"), model.OutboxStatusPending). + Limit(&limit64). + Sort(repository.Field("createdAt"), true) // true = ascending (oldest first) + + events := make([]*model.OutboxEvent, 0) + err := o.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.OutboxEvent{} + if err := cur.Decode(doc); err != nil { + return err + } + events = append(events, doc) + return nil + }) + if err != nil { + o.logger.Warn("failed to list pending outbox events", zap.Error(err)) + return nil, err + } + + o.logger.Debug("listed pending outbox events", zap.Int("count", len(events))) + return events, nil +} + +func (o *outboxStore) MarkSent(ctx context.Context, eventRef primitive.ObjectID, sentAt time.Time) error { + if eventRef.IsZero() { + o.logger.Warn("attempt to mark sent with zero event ID") + return merrors.InvalidArgument("outboxStore: zero event ID") + } + + patch := repository.Patch(). + Set(repository.Field("status"), model.OutboxStatusSent). + Set(repository.Field("sentAt"), sentAt) + + if err := o.repo.Patch(ctx, eventRef, patch); err != nil { + o.logger.Warn("failed to mark outbox event as sent", zap.Error(err), zap.String("eventRef", eventRef.Hex())) + return err + } + + o.logger.Debug("outbox event marked as sent", zap.String("eventRef", eventRef.Hex())) + return nil +} + +func (o *outboxStore) MarkFailed(ctx context.Context, eventRef primitive.ObjectID) error { + if eventRef.IsZero() { + o.logger.Warn("attempt to mark failed with zero event ID") + return merrors.InvalidArgument("outboxStore: zero event ID") + } + + patch := repository.Patch().Set(repository.Field("status"), model.OutboxStatusFailed) + + if err := o.repo.Patch(ctx, eventRef, patch); err != nil { + o.logger.Warn("failed to mark outbox event as failed", zap.Error(err), zap.String("eventRef", eventRef.Hex())) + return err + } + + o.logger.Debug("outbox event marked as failed", zap.String("eventRef", eventRef.Hex())) + return nil +} + +func (o *outboxStore) IncrementAttempts(ctx context.Context, eventRef primitive.ObjectID) error { + if eventRef.IsZero() { + o.logger.Warn("attempt to increment attempts with zero event ID") + return merrors.InvalidArgument("outboxStore: zero event ID") + } + + patch := repository.Patch().Inc(repository.Field("attempts"), 1) + + if err := o.repo.Patch(ctx, eventRef, patch); err != nil { + o.logger.Warn("failed to increment outbox attempts", zap.Error(err), zap.String("eventRef", eventRef.Hex())) + return err + } + + o.logger.Debug("outbox attempts incremented", zap.String("eventRef", eventRef.Hex())) + return nil +} diff --git a/api/ledger/storage/mongo/store/outbox_test.go b/api/ledger/storage/mongo/store/outbox_test.go new file mode 100644 index 0000000..e0adc62 --- /dev/null +++ b/api/ledger/storage/mongo/store/outbox_test.go @@ -0,0 +1,336 @@ +package store + +import ( + "context" + "errors" + "testing" + "time" + + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +func TestOutboxStore_Create(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + var insertedEvent *model.OutboxEvent + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + insertedEvent = object.(*model.OutboxEvent) + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + event := &model.OutboxEvent{ + EventID: "evt_12345", + Subject: "ledger.entry.created", + Payload: []byte(`{"entryId":"123"}`), + Status: model.OutboxStatusPending, + } + + err := store.Create(ctx, event) + + require.NoError(t, err) + assert.NotNil(t, insertedEvent) + assert.Equal(t, "evt_12345", insertedEvent.EventID) + assert.Equal(t, "ledger.entry.created", insertedEvent.Subject) + assert.Equal(t, model.OutboxStatusPending, insertedEvent.Status) + }) + + t.Run("NilEvent", func(t *testing.T) { + stub := &repositoryStub{} + store := &outboxStore{logger: logger, repo: stub} + + err := store.Create(ctx, nil) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("DuplicateEventID", func(t *testing.T) { + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return mongo.WriteException{ + WriteErrors: []mongo.WriteError{ + {Code: 11000}, // Duplicate key error + }, + } + }, + } + + store := &outboxStore{logger: logger, repo: stub} + event := &model.OutboxEvent{ + EventID: "duplicate_event", + Subject: "test.subject", + Status: model.OutboxStatusPending, + } + + err := store.Create(ctx, event) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrDataConflict)) + }) + + t.Run("InsertError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + InsertFunc: func(ctx context.Context, object storable.Storable, _ builder.Query) error { + return expectedErr + }, + } + + store := &outboxStore{logger: logger, repo: stub} + event := &model.OutboxEvent{ + EventID: "evt_123", + Subject: "test.subject", + } + + err := store.Create(ctx, event) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestOutboxStore_ListPending(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + results, err := store.ListPending(ctx, 10) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("EmptyResult", func(t *testing.T) { + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + results, err := store.ListPending(ctx, 10) + + require.NoError(t, err) + assert.Len(t, results, 0) + }) + + t.Run("WithLimit", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + results, err := store.ListPending(ctx, 3) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("FindError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return expectedErr + }, + } + + store := &outboxStore{logger: logger, repo: stub} + results, err := store.ListPending(ctx, 10) + + require.Error(t, err) + assert.Nil(t, results) + assert.Equal(t, expectedErr, err) + }) +} + +func TestOutboxStore_MarkSent(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + eventRef := primitive.NewObjectID() + sentTime := time.Now() + + t.Run("Success", func(t *testing.T) { + var patchedID primitive.ObjectID + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + patchedID = id + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + err := store.MarkSent(ctx, eventRef, sentTime) + + require.NoError(t, err) + assert.Equal(t, eventRef, patchedID) + }) + + t.Run("ZeroEventID", func(t *testing.T) { + stub := &repositoryStub{} + store := &outboxStore{logger: logger, repo: stub} + + err := store.MarkSent(ctx, primitive.NilObjectID, sentTime) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("PatchError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + return expectedErr + }, + } + + store := &outboxStore{logger: logger, repo: stub} + err := store.MarkSent(ctx, eventRef, sentTime) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestOutboxStore_MarkFailed(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + eventRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + var patchedID primitive.ObjectID + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + patchedID = id + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + err := store.MarkFailed(ctx, eventRef) + + require.NoError(t, err) + assert.Equal(t, eventRef, patchedID) + }) + + t.Run("ZeroEventID", func(t *testing.T) { + stub := &repositoryStub{} + store := &outboxStore{logger: logger, repo: stub} + + err := store.MarkFailed(ctx, primitive.NilObjectID) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("PatchError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + return expectedErr + }, + } + + store := &outboxStore{logger: logger, repo: stub} + err := store.MarkFailed(ctx, eventRef) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) +} + +func TestOutboxStore_IncrementAttempts(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + eventRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + var patchedID primitive.ObjectID + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + patchedID = id + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + err := store.IncrementAttempts(ctx, eventRef) + + require.NoError(t, err) + assert.Equal(t, eventRef, patchedID) + }) + + t.Run("ZeroEventID", func(t *testing.T) { + stub := &repositoryStub{} + store := &outboxStore{logger: logger, repo: stub} + + err := store.IncrementAttempts(ctx, primitive.NilObjectID) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("PatchError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + return expectedErr + }, + } + + store := &outboxStore{logger: logger, repo: stub} + err := store.IncrementAttempts(ctx, eventRef) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("MultipleIncrements", func(t *testing.T) { + var callCount int + stub := &repositoryStub{ + PatchFunc: func(ctx context.Context, id primitive.ObjectID, _ repository.PatchDoc) error { + callCount++ + return nil + }, + } + + store := &outboxStore{logger: logger, repo: stub} + + // Simulate multiple retry attempts + for i := 0; i < 3; i++ { + err := store.IncrementAttempts(ctx, eventRef) + require.NoError(t, err) + } + + assert.Equal(t, 3, callCount) + }) +} diff --git a/api/ledger/storage/mongo/store/posting_lines.go b/api/ledger/storage/mongo/store/posting_lines.go new file mode 100644 index 0000000..03c26df --- /dev/null +++ b/api/ledger/storage/mongo/store/posting_lines.go @@ -0,0 +1,138 @@ +package store + +import ( + "context" + + "github.com/tech/sendico/ledger/storage" + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type postingLinesStore struct { + logger mlogger.Logger + repo repository.Repository +} + +func NewPostingLines(logger mlogger.Logger, db *mongo.Database) (storage.PostingLinesStore, error) { + repo := repository.CreateMongoRepository(db, model.PostingLinesCollection) + + // Create index on journalEntryRef for fast lookup by entry + entryIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "journalEntryRef", Sort: ri.Asc}, + }, + } + if err := repo.CreateIndex(entryIndex); err != nil { + logger.Error("failed to ensure posting lines entry index", zap.Error(err)) + return nil, err + } + + // Create index on accountRef for account statement queries + accountIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "accountRef", Sort: ri.Asc}, + {Field: "createdAt", Sort: ri.Desc}, + }, + } + if err := repo.CreateIndex(accountIndex); err != nil { + logger.Error("failed to ensure posting lines account index", zap.Error(err)) + return nil, err + } + + childLogger := logger.Named(model.PostingLinesCollection) + childLogger.Debug("posting lines store initialised", zap.String("collection", model.PostingLinesCollection)) + + return &postingLinesStore{ + logger: childLogger, + repo: repo, + }, nil +} + +func (p *postingLinesStore) CreateMany(ctx context.Context, lines []*model.PostingLine) error { + if len(lines) == 0 { + p.logger.Warn("attempt to create empty posting lines array") + return nil + } + + storables := make([]storable.Storable, len(lines)) + for i, line := range lines { + if line == nil { + p.logger.Warn("attempt to create nil posting line") + return merrors.InvalidArgument("postingLinesStore: nil posting line") + } + storables[i] = line + } + + if err := p.repo.InsertMany(ctx, storables); err != nil { + p.logger.Warn("failed to create posting lines", zap.Error(err), zap.Int("count", len(lines))) + return err + } + + p.logger.Debug("posting lines created", zap.Int("count", len(lines))) + return nil +} + +func (p *postingLinesStore) ListByJournalEntry(ctx context.Context, entryRef primitive.ObjectID) ([]*model.PostingLine, error) { + if entryRef.IsZero() { + p.logger.Warn("attempt to list posting lines with zero entry ID") + return nil, merrors.InvalidArgument("postingLinesStore: zero entry ID") + } + + query := repository.Filter("journalEntryRef", entryRef) + + lines := make([]*model.PostingLine, 0) + err := p.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.PostingLine{} + if err := cur.Decode(doc); err != nil { + return err + } + lines = append(lines, doc) + return nil + }) + if err != nil { + p.logger.Warn("failed to list posting lines by entry", zap.Error(err), zap.String("entryRef", entryRef.Hex())) + return nil, err + } + + p.logger.Debug("listed posting lines by entry", zap.Int("count", len(lines)), zap.String("entryRef", entryRef.Hex())) + return lines, nil +} + +func (p *postingLinesStore) ListByAccount(ctx context.Context, accountRef primitive.ObjectID, limit int, offset int) ([]*model.PostingLine, error) { + if accountRef.IsZero() { + p.logger.Warn("attempt to list posting lines with zero account ID") + return nil, merrors.InvalidArgument("postingLinesStore: zero account ID") + } + + limit64 := int64(limit) + offset64 := int64(offset) + query := repository.Query(). + Filter(repository.Field("accountRef"), accountRef). + Limit(&limit64). + Offset(&offset64). + Sort(repository.Field("createdAt"), false) // false = descending + + lines := make([]*model.PostingLine, 0) + err := p.repo.FindManyByFilter(ctx, query, func(cur *mongo.Cursor) error { + doc := &model.PostingLine{} + if err := cur.Decode(doc); err != nil { + return err + } + lines = append(lines, doc) + return nil + }) + if err != nil { + p.logger.Warn("failed to list posting lines by account", zap.Error(err), zap.String("accountRef", accountRef.Hex())) + return nil, err + } + + p.logger.Debug("listed posting lines by account", zap.Int("count", len(lines)), zap.String("accountRef", accountRef.Hex())) + return lines, nil +} diff --git a/api/ledger/storage/mongo/store/posting_lines_test.go b/api/ledger/storage/mongo/store/posting_lines_test.go new file mode 100644 index 0000000..777bfd3 --- /dev/null +++ b/api/ledger/storage/mongo/store/posting_lines_test.go @@ -0,0 +1,276 @@ +package store + +import ( + "context" + "errors" + "testing" + + "github.com/tech/sendico/ledger/storage/model" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestPostingLinesStore_CreateMany(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + + t.Run("Success", func(t *testing.T) { + var insertedLines []storable.Storable + stub := &repositoryStub{ + InsertManyFunc: func(ctx context.Context, objects []storable.Storable) error { + insertedLines = objects + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + lines := []*model.PostingLine{ + { + JournalEntryRef: primitive.NewObjectID(), + AccountRef: primitive.NewObjectID(), + LineType: model.LineTypeMain, + Amount: "100.00", + }, + { + JournalEntryRef: primitive.NewObjectID(), + AccountRef: primitive.NewObjectID(), + LineType: model.LineTypeMain, + Amount: "100.00", + }, + } + + err := store.CreateMany(ctx, lines) + + require.NoError(t, err) + assert.Len(t, insertedLines, 2) + }) + + t.Run("EmptyArray", func(t *testing.T) { + stub := &repositoryStub{} + store := &postingLinesStore{logger: logger, repo: stub} + + err := store.CreateMany(ctx, []*model.PostingLine{}) + + require.NoError(t, err) // Should not error on empty array + }) + + t.Run("NilLine", func(t *testing.T) { + stub := &repositoryStub{} + store := &postingLinesStore{logger: logger, repo: stub} + + lines := []*model.PostingLine{ + {Amount: "100.00"}, + nil, + } + + err := store.CreateMany(ctx, lines) + + require.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("InsertManyError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + InsertManyFunc: func(ctx context.Context, objects []storable.Storable) error { + return expectedErr + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + lines := []*model.PostingLine{ + {Amount: "100.00"}, + } + + err := store.CreateMany(ctx, lines) + + require.Error(t, err) + assert.Equal(t, expectedErr, err) + }) + + t.Run("BalancedEntry", func(t *testing.T) { + var insertedLines []storable.Storable + stub := &repositoryStub{ + InsertManyFunc: func(ctx context.Context, objects []storable.Storable) error { + insertedLines = objects + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + entryRef := primitive.NewObjectID() + cashAccount := primitive.NewObjectID() + revenueAccount := primitive.NewObjectID() + + lines := []*model.PostingLine{ + { + JournalEntryRef: entryRef, + AccountRef: cashAccount, + LineType: model.LineTypeMain, + Amount: "500.00", + }, + { + JournalEntryRef: entryRef, + AccountRef: revenueAccount, + LineType: model.LineTypeMain, + Amount: "500.00", + }, + } + + err := store.CreateMany(ctx, lines) + + require.NoError(t, err) + assert.Len(t, insertedLines, 2) + }) +} + +func TestPostingLinesStore_ListByJournalEntry(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + entryRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByJournalEntry(ctx, entryRef) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("ZeroEntryID", func(t *testing.T) { + stub := &repositoryStub{} + store := &postingLinesStore{logger: logger, repo: stub} + + results, err := store.ListByJournalEntry(ctx, primitive.NilObjectID) + + require.Error(t, err) + assert.Nil(t, results) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("EmptyResult", func(t *testing.T) { + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByJournalEntry(ctx, entryRef) + + require.NoError(t, err) + assert.Len(t, results, 0) + }) + + t.Run("FindError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return expectedErr + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByJournalEntry(ctx, entryRef) + + require.Error(t, err) + assert.Nil(t, results) + assert.Equal(t, expectedErr, err) + }) +} + +func TestPostingLinesStore_ListByAccount(t *testing.T) { + ctx := context.Background() + logger := zap.NewNop() + accountRef := primitive.NewObjectID() + + t.Run("Success", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByAccount(ctx, accountRef, 10, 0) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("ZeroAccountID", func(t *testing.T) { + stub := &repositoryStub{} + store := &postingLinesStore{logger: logger, repo: stub} + + results, err := store.ListByAccount(ctx, primitive.NilObjectID, 10, 0) + + require.Error(t, err) + assert.Nil(t, results) + assert.True(t, errors.Is(err, merrors.ErrInvalidArg)) + }) + + t.Run("WithPagination", func(t *testing.T) { + called := false + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + called = true + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByAccount(ctx, accountRef, 2, 2) + + require.NoError(t, err) + assert.True(t, called) + assert.NotNil(t, results) + }) + + t.Run("EmptyResult", func(t *testing.T) { + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return nil + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByAccount(ctx, accountRef, 10, 0) + + require.NoError(t, err) + assert.Len(t, results, 0) + }) + + t.Run("FindError", func(t *testing.T) { + expectedErr := errors.New("database error") + stub := &repositoryStub{ + FindManyByFilterFunc: func(ctx context.Context, _ builder.Query, decoder rd.DecodingFunc) error { + return expectedErr + }, + } + + store := &postingLinesStore{logger: logger, repo: stub} + results, err := store.ListByAccount(ctx, accountRef, 10, 0) + + require.Error(t, err) + assert.Nil(t, results) + assert.Equal(t, expectedErr, err) + }) +} diff --git a/api/ledger/storage/mongo/store/testing_helpers_test.go b/api/ledger/storage/mongo/store/testing_helpers_test.go new file mode 100644 index 0000000..492e322 --- /dev/null +++ b/api/ledger/storage/mongo/store/testing_helpers_test.go @@ -0,0 +1,137 @@ +package store + +import ( + "context" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + rd "github.com/tech/sendico/pkg/db/repository/decoder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// repositoryStub provides a stub implementation of repository.Repository for testing +type repositoryStub struct { + AggregateFunc func(ctx context.Context, pipeline builder.Pipeline, decoder rd.DecodingFunc) error + GetFunc func(ctx context.Context, id primitive.ObjectID, result storable.Storable) error + InsertFunc func(ctx context.Context, object storable.Storable, filter builder.Query) error + InsertManyFunc func(ctx context.Context, objects []storable.Storable) error + UpdateFunc func(ctx context.Context, object storable.Storable) error + DeleteFunc func(ctx context.Context, id primitive.ObjectID) error + FindOneByFilterFunc func(ctx context.Context, filter builder.Query, result storable.Storable) error + FindManyByFilterFunc func(ctx context.Context, filter builder.Query, decoder rd.DecodingFunc) error + PatchFunc func(ctx context.Context, id primitive.ObjectID, patch repository.PatchDoc) error + PatchManyFunc func(ctx context.Context, filter repository.FilterQuery, patch repository.PatchDoc) (int, error) + DeleteManyFunc func(ctx context.Context, query builder.Query) error + ListIDsFunc func(ctx context.Context, query builder.Query) ([]primitive.ObjectID, error) + CreateIndexFunc func(def *ri.Definition) error +} + +func (r *repositoryStub) Aggregate(ctx context.Context, pipeline builder.Pipeline, decoder rd.DecodingFunc) error { + if r.AggregateFunc != nil { + return r.AggregateFunc(ctx, pipeline, decoder) + } + return nil +} + +func (r *repositoryStub) Get(ctx context.Context, id primitive.ObjectID, result storable.Storable) error { + if r.GetFunc != nil { + return r.GetFunc(ctx, id, result) + } + return nil +} + +func (r *repositoryStub) Insert(ctx context.Context, object storable.Storable, filter builder.Query) error { + if r.InsertFunc != nil { + return r.InsertFunc(ctx, object, filter) + } + return nil +} + +func (r *repositoryStub) InsertMany(ctx context.Context, objects []storable.Storable) error { + if r.InsertManyFunc != nil { + return r.InsertManyFunc(ctx, objects) + } + return nil +} + +func (r *repositoryStub) Update(ctx context.Context, object storable.Storable) error { + if r.UpdateFunc != nil { + return r.UpdateFunc(ctx, object) + } + return nil +} + +func (r *repositoryStub) Delete(ctx context.Context, id primitive.ObjectID) error { + if r.DeleteFunc != nil { + return r.DeleteFunc(ctx, id) + } + return nil +} + +func (r *repositoryStub) FindOneByFilter(ctx context.Context, filter builder.Query, result storable.Storable) error { + if r.FindOneByFilterFunc != nil { + return r.FindOneByFilterFunc(ctx, filter, result) + } + return nil +} + +func (r *repositoryStub) FindManyByFilter(ctx context.Context, filter builder.Query, decoder rd.DecodingFunc) error { + if r.FindManyByFilterFunc != nil { + return r.FindManyByFilterFunc(ctx, filter, decoder) + } + return nil +} + +func (r *repositoryStub) Patch(ctx context.Context, id primitive.ObjectID, patch repository.PatchDoc) error { + if r.PatchFunc != nil { + return r.PatchFunc(ctx, id, patch) + } + return nil +} + +func (r *repositoryStub) PatchMany(ctx context.Context, filter repository.FilterQuery, patch repository.PatchDoc) (int, error) { + if r.PatchManyFunc != nil { + return r.PatchManyFunc(ctx, filter, patch) + } + return 0, nil +} + +func (r *repositoryStub) DeleteMany(ctx context.Context, query builder.Query) error { + if r.DeleteManyFunc != nil { + return r.DeleteManyFunc(ctx, query) + } + return nil +} + +func (r *repositoryStub) ListIDs(ctx context.Context, query builder.Query) ([]primitive.ObjectID, error) { + if r.ListIDsFunc != nil { + return r.ListIDsFunc(ctx, query) + } + return nil, nil +} + +func (r *repositoryStub) ListPermissionBound(ctx context.Context, query builder.Query) ([]model.PermissionBoundStorable, error) { + return nil, nil +} + +func (r *repositoryStub) ListAccountBound(ctx context.Context, query builder.Query) ([]model.AccountBoundStorable, error) { + return nil, nil +} + +func (r *repositoryStub) Collection() string { + return "test_collection" +} + +func (r *repositoryStub) CreateIndex(def *ri.Definition) error { + if r.CreateIndexFunc != nil { + return r.CreateIndexFunc(def) + } + return nil +} + +// Note: For unit tests with FindManyByFilter, we don't simulate the full cursor iteration +// since we can't easily mock *mongo.Cursor. These tests verify that the store calls the +// repository correctly. Integration tests with real MongoDB test the actual iteration logic. diff --git a/api/ledger/storage/mongo/transaction.go b/api/ledger/storage/mongo/transaction.go new file mode 100644 index 0000000..64b7b65 --- /dev/null +++ b/api/ledger/storage/mongo/transaction.go @@ -0,0 +1,38 @@ +package mongo + +import ( + "context" + + "github.com/tech/sendico/pkg/db/transaction" + "go.mongodb.org/mongo-driver/mongo" +) + +type mongoTransactionFactory struct { + client *mongo.Client +} + +func (f *mongoTransactionFactory) CreateTransaction() transaction.Transaction { + return &mongoTransaction{client: f.client} +} + +type mongoTransaction struct { + client *mongo.Client +} + +func (t *mongoTransaction) Execute(ctx context.Context, cb transaction.Callback) (any, error) { + session, err := t.client.StartSession() + if err != nil { + return nil, err + } + defer session.EndSession(ctx) + + run := func(sessCtx mongo.SessionContext) (any, error) { + return cb(sessCtx) + } + + return session.WithTransaction(ctx, run) +} + +func newMongoTransactionFactory(client *mongo.Client) transaction.Factory { + return &mongoTransactionFactory{client: client} +} diff --git a/api/ledger/storage/repository.go b/api/ledger/storage/repository.go new file mode 100644 index 0000000..02e2fb8 --- /dev/null +++ b/api/ledger/storage/repository.go @@ -0,0 +1,14 @@ +package storage + +import "context" + +// Repository defines the main storage interface for ledger operations. +// It follows the fx/storage pattern with separate store interfaces for each collection. +type Repository interface { + Ping(ctx context.Context) error + Accounts() AccountsStore + JournalEntries() JournalEntriesStore + PostingLines() PostingLinesStore + Balances() BalancesStore + Outbox() OutboxStore +} diff --git a/api/ledger/storage/storage.go b/api/ledger/storage/storage.go new file mode 100644 index 0000000..f3eb21f --- /dev/null +++ b/api/ledger/storage/storage.go @@ -0,0 +1,61 @@ +package storage + +import ( + "context" + "time" + + "github.com/tech/sendico/ledger/storage/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type storageError string + +func (e storageError) Error() string { + return string(e) +} + +var ( + ErrAccountNotFound = storageError("ledger.storage: account not found") + ErrJournalEntryNotFound = storageError("ledger.storage: journal entry not found") + ErrBalanceNotFound = storageError("ledger.storage: balance not found") + ErrDuplicateIdempotency = storageError("ledger.storage: duplicate idempotency key") + ErrInsufficientBalance = storageError("ledger.storage: insufficient balance") + ErrAccountFrozen = storageError("ledger.storage: account is frozen") + ErrNegativeBalancePolicy = storageError("ledger.storage: negative balance not allowed") +) + +type AccountsStore interface { + Create(ctx context.Context, account *model.Account) error + Get(ctx context.Context, accountRef primitive.ObjectID) (*model.Account, error) + GetByAccountCode(ctx context.Context, orgRef primitive.ObjectID, accountCode, currency string) (*model.Account, error) + GetDefaultSettlement(ctx context.Context, orgRef primitive.ObjectID, currency string) (*model.Account, error) + ListByOrganization(ctx context.Context, orgRef primitive.ObjectID, limit int, offset int) ([]*model.Account, error) + UpdateStatus(ctx context.Context, accountRef primitive.ObjectID, status model.AccountStatus) error +} + +type JournalEntriesStore interface { + Create(ctx context.Context, entry *model.JournalEntry) error + Get(ctx context.Context, entryRef primitive.ObjectID) (*model.JournalEntry, error) + GetByIdempotencyKey(ctx context.Context, orgRef primitive.ObjectID, idempotencyKey string) (*model.JournalEntry, error) + ListByOrganization(ctx context.Context, orgRef primitive.ObjectID, limit int, offset int) ([]*model.JournalEntry, error) +} + +type PostingLinesStore interface { + CreateMany(ctx context.Context, lines []*model.PostingLine) error + ListByJournalEntry(ctx context.Context, entryRef primitive.ObjectID) ([]*model.PostingLine, error) + ListByAccount(ctx context.Context, accountRef primitive.ObjectID, limit int, offset int) ([]*model.PostingLine, error) +} + +type BalancesStore interface { + Get(ctx context.Context, accountRef primitive.ObjectID) (*model.AccountBalance, error) + Upsert(ctx context.Context, balance *model.AccountBalance) error + IncrementBalance(ctx context.Context, accountRef primitive.ObjectID, amount string) error +} + +type OutboxStore interface { + Create(ctx context.Context, event *model.OutboxEvent) error + ListPending(ctx context.Context, limit int) ([]*model.OutboxEvent, error) + MarkSent(ctx context.Context, eventRef primitive.ObjectID, sentAt time.Time) error + MarkFailed(ctx context.Context, eventRef primitive.ObjectID) error + IncrementAttempts(ctx context.Context, eventRef primitive.ObjectID) error +} diff --git a/api/payments/orchestrator/.gitignore b/api/payments/orchestrator/.gitignore new file mode 100644 index 0000000..c62beb6 --- /dev/null +++ b/api/payments/orchestrator/.gitignore @@ -0,0 +1,3 @@ +internal/generated +.gocache +app diff --git a/api/payments/orchestrator/client/client.go b/api/payments/orchestrator/client/client.go new file mode 100644 index 0000000..e98b3ee --- /dev/null +++ b/api/payments/orchestrator/client/client.go @@ -0,0 +1,148 @@ +package client + +import ( + "context" + "crypto/tls" + "errors" + "fmt" + "strings" + "time" + + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Client exposes typed helpers around the payment orchestrator gRPC API. +type Client interface { + QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) + InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) + CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) + GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) + ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) + InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) + ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) + ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) + Close() error +} + +type grpcOrchestratorClient interface { + QuotePayment(ctx context.Context, in *orchestratorv1.QuotePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.QuotePaymentResponse, error) + InitiatePayment(ctx context.Context, in *orchestratorv1.InitiatePaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiatePaymentResponse, error) + CancelPayment(ctx context.Context, in *orchestratorv1.CancelPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.CancelPaymentResponse, error) + GetPayment(ctx context.Context, in *orchestratorv1.GetPaymentRequest, opts ...grpc.CallOption) (*orchestratorv1.GetPaymentResponse, error) + ListPayments(ctx context.Context, in *orchestratorv1.ListPaymentsRequest, opts ...grpc.CallOption) (*orchestratorv1.ListPaymentsResponse, error) + InitiateConversion(ctx context.Context, in *orchestratorv1.InitiateConversionRequest, opts ...grpc.CallOption) (*orchestratorv1.InitiateConversionResponse, error) + ProcessTransferUpdate(ctx context.Context, in *orchestratorv1.ProcessTransferUpdateRequest, opts ...grpc.CallOption) (*orchestratorv1.ProcessTransferUpdateResponse, error) + ProcessDepositObserved(ctx context.Context, in *orchestratorv1.ProcessDepositObservedRequest, opts ...grpc.CallOption) (*orchestratorv1.ProcessDepositObservedResponse, error) +} + +type orchestratorClient struct { + cfg Config + conn *grpc.ClientConn + client grpcOrchestratorClient +} + +// New dials the payment orchestrator endpoint and returns a ready client. +func New(ctx context.Context, cfg Config, opts ...grpc.DialOption) (Client, error) { + cfg.setDefaults() + if strings.TrimSpace(cfg.Address) == "" { + return nil, errors.New("payment-orchestrator: address is required") + } + + dialCtx, cancel := context.WithTimeout(ctx, cfg.DialTimeout) + defer cancel() + + dialOpts := make([]grpc.DialOption, 0, len(opts)+1) + dialOpts = append(dialOpts, opts...) + + if cfg.Insecure { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(insecure.NewCredentials())) + } else { + dialOpts = append(dialOpts, grpc.WithTransportCredentials(credentials.NewTLS(&tls.Config{}))) + } + + conn, err := grpc.DialContext(dialCtx, cfg.Address, dialOpts...) + if err != nil { + return nil, fmt.Errorf("payment-orchestrator: dial %s: %w", cfg.Address, err) + } + + return &orchestratorClient{ + cfg: cfg, + conn: conn, + client: orchestratorv1.NewPaymentOrchestratorClient(conn), + }, nil +} + +// NewWithClient injects a pre-built orchestrator client (useful for tests). +func NewWithClient(cfg Config, oc grpcOrchestratorClient) Client { + cfg.setDefaults() + return &orchestratorClient{ + cfg: cfg, + client: oc, + } +} + +func (c *orchestratorClient) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +func (c *orchestratorClient) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.QuotePayment(ctx, req) +} + +func (c *orchestratorClient) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.InitiatePayment(ctx, req) +} + +func (c *orchestratorClient) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.CancelPayment(ctx, req) +} + +func (c *orchestratorClient) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetPayment(ctx, req) +} + +func (c *orchestratorClient) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ListPayments(ctx, req) +} + +func (c *orchestratorClient) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.InitiateConversion(ctx, req) +} + +func (c *orchestratorClient) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ProcessTransferUpdate(ctx, req) +} + +func (c *orchestratorClient) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) { + ctx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ProcessDepositObserved(ctx, req) +} + +func (c *orchestratorClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + return context.WithTimeout(ctx, timeout) +} diff --git a/api/payments/orchestrator/client/config.go b/api/payments/orchestrator/client/config.go new file mode 100644 index 0000000..9255d80 --- /dev/null +++ b/api/payments/orchestrator/client/config.go @@ -0,0 +1,20 @@ +package client + +import "time" + +// Config captures connection settings for the payment orchestrator gRPC service. +type Config struct { + Address string + DialTimeout time.Duration + CallTimeout time.Duration + Insecure bool +} + +func (c *Config) setDefaults() { + if c.DialTimeout <= 0 { + c.DialTimeout = 5 * time.Second + } + if c.CallTimeout <= 0 { + c.CallTimeout = 3 * time.Second + } +} diff --git a/api/payments/orchestrator/client/fake.go b/api/payments/orchestrator/client/fake.go new file mode 100644 index 0000000..f5fd1e9 --- /dev/null +++ b/api/payments/orchestrator/client/fake.go @@ -0,0 +1,83 @@ +package client + +import ( + "context" + + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" +) + +// Fake implements Client for tests. +type Fake struct { + QuotePaymentFn func(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) + InitiatePaymentFn func(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) + CancelPaymentFn func(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) + GetPaymentFn func(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) + ListPaymentsFn func(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) + InitiateConversionFn func(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) + ProcessTransferUpdateFn func(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) + ProcessDepositObservedFn func(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) + CloseFn func() error +} + +func (f *Fake) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) { + if f.QuotePaymentFn != nil { + return f.QuotePaymentFn(ctx, req) + } + return &orchestratorv1.QuotePaymentResponse{}, nil +} + +func (f *Fake) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) { + if f.InitiatePaymentFn != nil { + return f.InitiatePaymentFn(ctx, req) + } + return &orchestratorv1.InitiatePaymentResponse{}, nil +} + +func (f *Fake) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) { + if f.CancelPaymentFn != nil { + return f.CancelPaymentFn(ctx, req) + } + return &orchestratorv1.CancelPaymentResponse{}, nil +} + +func (f *Fake) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) { + if f.GetPaymentFn != nil { + return f.GetPaymentFn(ctx, req) + } + return &orchestratorv1.GetPaymentResponse{}, nil +} + +func (f *Fake) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) { + if f.ListPaymentsFn != nil { + return f.ListPaymentsFn(ctx, req) + } + return &orchestratorv1.ListPaymentsResponse{}, nil +} + +func (f *Fake) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) { + if f.InitiateConversionFn != nil { + return f.InitiateConversionFn(ctx, req) + } + return &orchestratorv1.InitiateConversionResponse{}, nil +} + +func (f *Fake) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) { + if f.ProcessTransferUpdateFn != nil { + return f.ProcessTransferUpdateFn(ctx, req) + } + return &orchestratorv1.ProcessTransferUpdateResponse{}, nil +} + +func (f *Fake) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) { + if f.ProcessDepositObservedFn != nil { + return f.ProcessDepositObservedFn(ctx, req) + } + return &orchestratorv1.ProcessDepositObservedResponse{}, nil +} + +func (f *Fake) Close() error { + if f.CloseFn != nil { + return f.CloseFn() + } + return nil +} diff --git a/api/payments/orchestrator/env/.gitignore b/api/payments/orchestrator/env/.gitignore new file mode 100644 index 0000000..f2a8cbe --- /dev/null +++ b/api/payments/orchestrator/env/.gitignore @@ -0,0 +1 @@ +.env.api diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod new file mode 100644 index 0000000..7931eff --- /dev/null +++ b/api/payments/orchestrator/go.mod @@ -0,0 +1,60 @@ +module github.com/tech/sendico/payments/orchestrator + +go 1.25.3 + +replace github.com/tech/sendico/pkg => ../../pkg + +replace github.com/tech/sendico/billing/fees => ../../billing/fees + +replace github.com/tech/sendico/chain/gateway => ../../chain/gateway + +replace github.com/tech/sendico/fx/oracle => ../../fx/oracle + +replace github.com/tech/sendico/ledger => ../../ledger + +require ( + github.com/prometheus/client_golang v1.23.2 + github.com/shopspring/decimal v1.4.0 + github.com/tech/sendico/chain/gateway v0.0.0-00010101000000-000000000000 + github.com/tech/sendico/fx/oracle v0.0.0-00010101000000-000000000000 + github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000 + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver v1.17.6 + go.uber.org/zap v1.27.0 + google.golang.org/grpc v1.76.0 + google.golang.org/protobuf v1.36.10 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.9.1 // indirect + github.com/casbin/casbin/v2 v2.132.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v3 v3.7.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-chi/chi/v5 v5.2.3 // indirect + github.com/golang/snappy v1.0.0 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.1 // indirect + github.com/mitchellh/mapstructure v1.5.0 // indirect + github.com/montanaflynn/stats v0.7.1 // indirect + github.com/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.47.0 // indirect + github.com/nats-io/nkeys v0.4.11 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.2 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.1.2 // indirect + github.com/xdg-go/stringprep v1.0.4 // indirect + github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect + go.uber.org/multierr v1.11.0 // indirect + go.yaml.in/yaml/v2 v2.4.3 // indirect + golang.org/x/crypto v0.43.0 // indirect + golang.org/x/net v0.46.0 // indirect + golang.org/x/sync v0.17.0 // indirect + golang.org/x/sys v0.37.0 // indirect + golang.org/x/text v0.30.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 // indirect +) diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum new file mode 100644 index 0000000..b4e0240 --- /dev/null +++ b/api/payments/orchestrator/go.sum @@ -0,0 +1,225 @@ +dario.cat/mergo v1.0.1 h1:Ra4+bf83h2ztPIQYNP99R6m+Y7KfnARDfID+a+vLl4s= +dario.cat/mergo v1.0.1/go.mod h1:uNxQE+84aUszobStD9th8a29P2fMDhsBdgRYvZOxGmk= +github.com/Azure/go-ansiterm v0.0.0-20230124172434-306776ec8161 h1:L/gRVlceqvL25UVaW/CKtUDjefjrs0SPonmDGUVOYP0= +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/go.mod h1:yd8OoFMLzJbo9gZq8j5qaps8bJ9aShtEA8Ipt1oGCvU= +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/bmatcuk/doublestar/v4 v4.6.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/bmatcuk/doublestar/v4 v4.9.1 h1:X8jg9rRZmJd4yRy7ZeNDRnM+T3ZfHv15JiBJ/avrEXE= +github.com/bmatcuk/doublestar/v4 v4.9.1/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.132.0 h1:73hGmOszGSL3hTVquwkAi98XLl3gPJ+BxB6D7G9Fxtk= +github.com/casbin/casbin/v2 v2.132.0/go.mod h1:FmcfntdXLTcYXv/hxgNntcRPqAbwOG9xsism0yXT+18= +github.com/casbin/govaluate v1.3.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/govaluate v1.10.0 h1:ffGw51/hYH3w3rZcxO/KcaUIDOLP84w7nsidMVgaDG0= +github.com/casbin/govaluate v1.10.0/go.mod h1:G/UnbIjZk/0uMNaLwZZmFQrR72tYRZWQkO70si/iR7A= +github.com/casbin/mongodb-adapter/v3 v3.7.0 h1:w9c3bea1BGK4eZTAmk17JkY52yv/xSZDSHKji8q+z6E= +github.com/casbin/mongodb-adapter/v3 v3.7.0/go.mod h1:F1mu4ojoJVE/8VhIMxMedhjfwRDdIXgANYs6Sd0MgVA= +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/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs= +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/go.mod h1:VRRf09a7mHDIRezVKTRCrOq78v577GXq3bSa3EhrzVo= +github.com/containerd/platforms v0.2.1 h1:zvwtM3rz2YHPQsF2CHYM8+KtB5dvhISiXh5ZpSBQv6A= +github.com/containerd/platforms v0.2.1/go.mod h1:XHCb+2/hzowdiut9rkudds9bE5yJ7npe7dG/wG+uFPw= +github.com/cpuguy83/dockercfg v0.3.2 h1:DlJTyZGBDlXqUZ2Dk2Q3xHs/FtnooJJVaad2S9GKorA= +github.com/cpuguy83/dockercfg v0.3.2/go.mod h1:sugsbF4//dDlL/i+S+rtpIWp+5h0BHJHfjj5/jFyUJc= +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/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/docker/docker v27.3.1+incompatible h1:KttF0XoteNTicmUtBO0L2tP+J7FGRFTjaEF4k6WdhfI= +github.com/docker/docker v27.3.1+incompatible/go.mod h1:eEKB0N0r5NX/I1kEveEz05bcu8tLC/8azJZsviup8Sk= +github.com/docker/go-connections v0.5.0 h1:USnMq7hx7gwdVZq1L49hLXaFtUdTADjXGp+uj1Br63c= +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/go.mod h1:fgPhTUdO+D/Jk86RDLlptpiXQzgHJF7gydDDbaIK4Dk= +github.com/felixge/httpsnoop v1.0.4 h1:NFTV2Zj1bL4mc9sqWACXbQFVBBg2W3GPvqp8/ESS2Wg= +github.com/felixge/httpsnoop v1.0.4/go.mod h1:m8KPJKqk1gH5J9DgRY2ASl2lWCfGKXixSwevea8zH2U= +github.com/go-chi/chi/v5 v5.2.3 h1:WQIt9uxdsAbgIYgid+BpYc+liqQZGMHRaUwp0JUcvdE= +github.com/go-chi/chi/v5 v5.2.3/go.mod h1:L2yAIGWB3H+phAw1NxKwWM+7eUH/lU8pOMm5hHcoops= +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/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= +github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE= +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/gogo/protobuf v1.3.2 h1:Ov1cvc58UF3b5XjBnZv7+opcTcQFZebYjWzi34vdm4Q= +github.com/gogo/protobuf v1.3.2/go.mod h1:P1XiOD3dCwIKUDQYPy72D8LYyHL2YPYrpS2s69NZV8Q= +github.com/golang/mock v1.4.4/go.mod h1:l3mdAwkq5BuhzHwde/uurv3sEJeZMXNpwsxVWU71h+4= +github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= +github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs= +github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek= +github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps= +github.com/golang/snappy v1.0.0 h1:Oy607GVXHs7RtbggtPBnr2RmDArIsAefDwvrdWvRhGs= +github.com/golang/snappy v1.0.0/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q= +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/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0= +github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo= +github.com/klauspost/compress v1.18.1 h1:bcSGx7UbpBqMChDtsF28Lw6v/G94LPrrbMbdC3JH2co= +github.com/klauspost/compress v1.18.1/go.mod h1:ZQFFVG+MdnR0P+l6wpXgIL4NTtwiKIdBnrBd8Nrxr+0= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +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/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= +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/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY= +github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0= +github.com/mattn/go-colorable v0.1.14 h1:9A9LHSqF/7dyVVX6g0U9cwm9pG3kP9gSzcuIPHPsaIE= +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/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= +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/moby/docker-image-spec v1.3.1 h1:jMKff3w6PgbfSa69GfNg+zN/XLhfXJGnEx3Nl2EsFP0= +github.com/moby/docker-image-spec v1.3.1/go.mod h1:eKmb5VW8vQEh/BAr2yvVNvuiJuY6UIocYsFu/DxxRpo= +github.com/moby/patternmatcher v0.6.0 h1:GmP9lR19aU5GqSSFko+5pRqHi+Ohk1O69aFiKkVGiPk= +github.com/moby/patternmatcher v0.6.0/go.mod h1:hDPoyOpDY7OrrMDLaYoY3hf52gNCR/YOUYxkhApJIxc= +github.com/moby/sys/sequential v0.6.0 h1:qrx7XFUd/5DxtqcoH1h438hF5TmOvzC/lspjy7zgvCU= +github.com/moby/sys/sequential v0.6.0/go.mod h1:uyv8EUTrca5PnDsdMGXhZe6CCe8U/UiTWd+lL+7b/Ko= +github.com/moby/sys/user v0.3.0 h1:9ni5DlcW5an3SvRSx4MouotOygvzaXbaSrc/wGDFWPo= +github.com/moby/sys/user v0.3.0/go.mod h1:bG+tYYYJgaMtRKgEmuueC0hJEAZWwtIbZTB+85uoHjs= +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/term v0.5.0 h1:xt8Q1nalod/v7BqbG21f8mQPqH+xAaC9C3N3wfWbVP0= +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.7.1/go.mod h1:etXPPgVO6n31NxCd9KQUMvCM+ve0ruNzt6R8Bnaayow= +github.com/morikuni/aec v1.0.0 h1:nP9CBfwrvYnBRgY6qfDQkygYDmYwOilePFkwzv4dU8A= +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/go.mod h1:+n7T8mK8HuQTcFwEeznm/DIxMOiR9yIdICNftLE1DvQ= +github.com/nats-io/nats.go v1.47.0 h1:YQdADw6J/UfGUd2Oy6tn4Hq6YHxCaJrVKayxxFqYrgM= +github.com/nats-io/nats.go v1.47.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.11 h1:q44qGV008kYd9W1b1nEBkNzvnWxtRSQ7A8BoqRrcfa0= +github.com/nats-io/nkeys v0.4.11/go.mod h1:szDimtgmfOi9n25JpfIdGw12tZFYXqhGxjhVxsatHVE= +github.com/nats-io/nuid v1.0.1 h1:5iA8DT8V7q8WK2EScv2padNa/rTESc1KdnPw4TC2paw= +github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= +github.com/opencontainers/go-digest v1.0.0 h1:apOUWs51W5PlhuyGyz9FCeeBIOUDA/6nW8Oi/yOhh5U= +github.com/opencontainers/go-digest v1.0.0/go.mod h1:0JzlMkj0TRzQZfJkVvzbP0HBR3IKzErnv2BNG4W4MAM= +github.com/opencontainers/image-spec v1.1.0 h1:8SG7/vwALn54lVB/0yZ/MMwhFrPYtpEHQb2IpWsCzug= +github.com/opencontainers/image-spec v1.1.0/go.mod h1:W4s4sFTMaBeK1BQLXbG4AdM2szdn85PY75RI83NrTrM= +github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= +github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= +github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= +github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU= +github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= +github.com/prometheus/client_golang v1.23.2 h1:Je96obch5RDVy3FDMndoUsjAhG5Edi49h0RJWRi/o0o= +github.com/prometheus/client_golang v1.23.2/go.mod h1:Tb1a6LWHB3/SPIzCoaDXI4I8UHKeFTEQ1YCr+0Gyqmg= +github.com/prometheus/client_model v0.6.2 h1:oBsgwpGs7iVziMvrGhE53c/GrLUsZdHnqNwqPLxwZyk= +github.com/prometheus/client_model v0.6.2/go.mod h1:y3m2F6Gdpfy6Ut/GBsUqTWZqCUvMVzSfMLjcu6wAwpE= +github.com/prometheus/common v0.67.2 h1:PcBAckGFTIHt2+L3I33uNRTlKTplNzFctXcWhPyAEN8= +github.com/prometheus/common v0.67.2/go.mod h1:63W3KZb1JOKgcjlIr64WW/LvFGAqKPj0atm+knVGEko= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.19.2/go.mod h1:M0aotyiemPhBCM0z5w87kL22CxfcH05ZpYlu+b4J7mw= +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/shirou/gopsutil v3.21.11+incompatible h1:+1+c1VGhc88SSonWP6foOcLhvnKlUeu/erjjvaPEYiI= +github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI= +github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk= +github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= +github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= +github.com/shopspring/decimal v1.4.0 h1:bxl37RwXBklmTi0C79JfXCEBD1cqqHt0bbgBAGFp81k= +github.com/shopspring/decimal v1.4.0/go.mod h1:gawqmDU56v4yIKSwfBSFip1HdCCXN8/+DMd9qYNcwME= +github.com/sirupsen/logrus v1.9.3 h1:dueUQJ1C2q9oE3F7wvmSGAaVtTmUizReu6fjN8uqzbQ= +github.com/sirupsen/logrus v1.9.3/go.mod h1:naHLuLoDiP4jHNo9R0sCBMtWGeIprob74mVsIT4qYEQ= +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/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U= +github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U= +github.com/testcontainers/testcontainers-go v0.33.0 h1:zJS9PfXYT5O0ZFXM2xxXfk4J5UMw/kRiISng037Gxdw= +github.com/testcontainers/testcontainers-go v0.33.0/go.mod h1:W80YpTa8D5C3Yy16icheD01UTDu+LmXIA2Keo+jWtT8= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0 h1:iXVA84s5hKMS5gn01GWOYHE3ymy/2b+0YkpFeTxB2XY= +github.com/testcontainers/testcontainers-go/modules/mongodb v0.33.0/go.mod h1:R6tMjTojRiaoo89fh/hf7tOmfzohdqSU17R9DwSVSog= +github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4= +github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4= +github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso= +github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ= +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/scram v1.1.2 h1:FHX5I5B4i4hKRVRBCFRxq1iQRej7WO3hhBuJf+UUySY= +github.com/xdg-go/scram v1.1.2/go.mod h1:RT/sEzTbU5y00aCK8UOx6R7YryM0iF1N2MOmC3kKLN4= +github.com/xdg-go/stringprep v1.0.4 h1:XLI/Ng3O1Atzq0oBs3TWm+5ZVgkq2aqdlvP9JtoZ6c8= +github.com/xdg-go/stringprep v1.0.4/go.mod h1:mPGuuIYwz7CmR2bT9j4GbQqutWS1zV24gijq1dTyGkM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 h1:ilQV1hzziu+LLM3zUTJ0trRztfwgjqKnBWNtSRkbmwM= +github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78/go.mod h1:aL8wCCfTfSfmXjznFBSZNN13rSJjlIOI1fUNAtF7rmI= +github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= +github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0= +github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= +go.mongodb.org/mongo-driver v1.17.6 h1:87JUG1wZfWsr6rIz3ZmpH90rL5tea7O3IHuSwHUpsss= +go.mongodb.org/mongo-driver v1.17.6/go.mod h1:Hy04i7O2kC4RS06ZrhPRqj/u4DTYkFDAAccj+rVKqgQ= +go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA= +go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A= +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/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ= +go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I= +go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE= +go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E= +go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI= +go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg= +go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc= +go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps= +go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4= +go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0= +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/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= +go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y= +go.uber.org/zap v1.27.0 h1:aJMhYGrd5QSmlpLMr2MftRKl7t8J8PTZPA732ud/XR8= +go.uber.org/zap v1.27.0/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= +golang.org/x/crypto v0.43.0 h1:dduJYIi3A3KOfdGOHX8AVZ/jGiyPa3IbBozJ5kNuE04= +golang.org/x/crypto v0.43.0/go.mod h1:BFbav4mRNlXJL4wNeejLpWxB7wMbc79PdRGhWKncxR0= +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-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-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= +golang.org/x/net v0.46.0 h1:giFlY12I07fugqwPuWJi68oOnpfqFnJIJzaIIm2JVV4= +golang.org/x/net v0.46.0/go.mod h1:Q9BGdFy1y4nkUwiLvT5qtyhAnEHgnQ/zd8PfU6nc210= +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.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug= +golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= +golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= +golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= +golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= +golang.org/x/sys v0.37.0 h1:fdNQudmxPjkdUTPnLn5mdQv7Zwvbvpaxqs831goi9kQ= +golang.org/x/sys v0.37.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-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.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.8/go.mod h1:E6s5w1FMmriuDzIBO73fBruAKo1PCIq6d2Q6DHfQ8WQ= +golang.org/x/text v0.30.0 h1:yznKA/E9zq54KzlzBEAWn1NXSQ8DIp/NYMy88xJjl4k= +golang.org/x/text v0.30.0/go.mod h1:yDdHFIX9t+tORqspjENWgzaCVXgk0yYnYuSZ8UzzBVM= +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-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= +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= +gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= +gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101 h1:tRPGkdGHuewF4UisLzzHHr1spKw92qLM98nIzxbC0wY= +google.golang.org/genproto/googleapis/rpc v0.0.0-20251103181224-f26f9409b101/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk= +google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A= +google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c= +google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE= +google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= +gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= +gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= +gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/api/payments/orchestrator/internal/service/orchestrator/convert.go b/api/payments/orchestrator/internal/service/orchestrator/convert.go new file mode 100644 index 0000000..de026af --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/convert.go @@ -0,0 +1,426 @@ +package orchestrator + +import ( + "strings" + "time" + + "github.com/tech/sendico/payments/orchestrator/storage/model" + "github.com/tech/sendico/pkg/merrors" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func intentFromProto(src *orchestratorv1.PaymentIntent) model.PaymentIntent { + if src == nil { + return model.PaymentIntent{} + } + intent := model.PaymentIntent{ + Kind: modelKindFromProto(src.GetKind()), + Source: endpointFromProto(src.GetSource()), + Destination: endpointFromProto(src.GetDestination()), + Amount: cloneMoney(src.GetAmount()), + RequiresFX: src.GetRequiresFx(), + FeePolicy: src.GetFeePolicy(), + Attributes: cloneMetadata(src.GetAttributes()), + } + if src.GetFx() != nil { + intent.FX = fxIntentFromProto(src.GetFx()) + } + return intent +} + +func endpointFromProto(src *orchestratorv1.PaymentEndpoint) model.PaymentEndpoint { + if src == nil { + return model.PaymentEndpoint{Type: model.EndpointTypeUnspecified} + } + result := model.PaymentEndpoint{ + Type: model.EndpointTypeUnspecified, + Metadata: cloneMetadata(src.GetMetadata()), + } + if ledger := src.GetLedger(); ledger != nil { + result.Type = model.EndpointTypeLedger + result.Ledger = &model.LedgerEndpoint{ + LedgerAccountRef: strings.TrimSpace(ledger.GetLedgerAccountRef()), + ContraLedgerAccountRef: strings.TrimSpace(ledger.GetContraLedgerAccountRef()), + } + return result + } + if managed := src.GetManagedWallet(); managed != nil { + result.Type = model.EndpointTypeManagedWallet + result.ManagedWallet = &model.ManagedWalletEndpoint{ + ManagedWalletRef: strings.TrimSpace(managed.GetManagedWalletRef()), + Asset: cloneAsset(managed.GetAsset()), + } + return result + } + if external := src.GetExternalChain(); external != nil { + result.Type = model.EndpointTypeExternalChain + result.ExternalChain = &model.ExternalChainEndpoint{ + Asset: cloneAsset(external.GetAsset()), + Address: strings.TrimSpace(external.GetAddress()), + Memo: strings.TrimSpace(external.GetMemo()), + } + return result + } + return result +} + +func fxIntentFromProto(src *orchestratorv1.FXIntent) *model.FXIntent { + if src == nil { + return nil + } + return &model.FXIntent{ + Pair: clonePair(src.GetPair()), + Side: src.GetSide(), + Firm: src.GetFirm(), + TTLMillis: src.GetTtlMs(), + PreferredProvider: strings.TrimSpace(src.GetPreferredProvider()), + MaxAgeMillis: src.GetMaxAgeMs(), + } +} + +func quoteSnapshotToModel(src *orchestratorv1.PaymentQuote) *model.PaymentQuoteSnapshot { + if src == nil { + return nil + } + return &model.PaymentQuoteSnapshot{ + DebitAmount: cloneMoney(src.GetDebitAmount()), + ExpectedSettlementAmount: cloneMoney(src.GetExpectedSettlementAmount()), + ExpectedFeeTotal: cloneMoney(src.GetExpectedFeeTotal()), + FeeLines: cloneFeeLines(src.GetFeeLines()), + FeeRules: cloneFeeRules(src.GetFeeRules()), + FXQuote: cloneFXQuote(src.GetFxQuote()), + NetworkFee: cloneNetworkEstimate(src.GetNetworkFee()), + FeeQuoteToken: strings.TrimSpace(src.GetFeeQuoteToken()), + } +} + +func toProtoPayment(src *model.Payment) *orchestratorv1.Payment { + if src == nil { + return nil + } + payment := &orchestratorv1.Payment{ + PaymentRef: src.PaymentRef, + IdempotencyKey: src.IdempotencyKey, + Intent: protoIntentFromModel(src.Intent), + State: protoStateFromModel(src.State), + FailureCode: protoFailureFromModel(src.FailureCode), + FailureReason: src.FailureReason, + LastQuote: modelQuoteToProto(src.LastQuote), + Execution: protoExecutionFromModel(src.Execution), + Metadata: cloneMetadata(src.Metadata), + } + if src.CreatedAt.IsZero() { + payment.CreatedAt = timestamppb.New(time.Now().UTC()) + } else { + payment.CreatedAt = timestamppb.New(src.CreatedAt.UTC()) + } + if src.UpdatedAt != (time.Time{}) { + payment.UpdatedAt = timestamppb.New(src.UpdatedAt.UTC()) + } + return payment +} + +func protoIntentFromModel(src model.PaymentIntent) *orchestratorv1.PaymentIntent { + intent := &orchestratorv1.PaymentIntent{ + Kind: protoKindFromModel(src.Kind), + Source: protoEndpointFromModel(src.Source), + Destination: protoEndpointFromModel(src.Destination), + Amount: cloneMoney(src.Amount), + RequiresFx: src.RequiresFX, + FeePolicy: src.FeePolicy, + Attributes: cloneMetadata(src.Attributes), + } + if src.FX != nil { + intent.Fx = protoFXIntentFromModel(src.FX) + } + return intent +} + +func protoEndpointFromModel(src model.PaymentEndpoint) *orchestratorv1.PaymentEndpoint { + endpoint := &orchestratorv1.PaymentEndpoint{ + Metadata: cloneMetadata(src.Metadata), + } + switch src.Type { + case model.EndpointTypeLedger: + if src.Ledger != nil { + endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_Ledger{ + Ledger: &orchestratorv1.LedgerEndpoint{ + LedgerAccountRef: src.Ledger.LedgerAccountRef, + ContraLedgerAccountRef: src.Ledger.ContraLedgerAccountRef, + }, + } + } + case model.EndpointTypeManagedWallet: + if src.ManagedWallet != nil { + endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_ManagedWallet{ + ManagedWallet: &orchestratorv1.ManagedWalletEndpoint{ + ManagedWalletRef: src.ManagedWallet.ManagedWalletRef, + Asset: cloneAsset(src.ManagedWallet.Asset), + }, + } + } + case model.EndpointTypeExternalChain: + if src.ExternalChain != nil { + endpoint.Endpoint = &orchestratorv1.PaymentEndpoint_ExternalChain{ + ExternalChain: &orchestratorv1.ExternalChainEndpoint{ + Asset: cloneAsset(src.ExternalChain.Asset), + Address: src.ExternalChain.Address, + Memo: src.ExternalChain.Memo, + }, + } + } + default: + // leave unspecified + } + return endpoint +} + +func protoFXIntentFromModel(src *model.FXIntent) *orchestratorv1.FXIntent { + if src == nil { + return nil + } + return &orchestratorv1.FXIntent{ + Pair: clonePair(src.Pair), + Side: src.Side, + Firm: src.Firm, + TtlMs: src.TTLMillis, + PreferredProvider: src.PreferredProvider, + MaxAgeMs: src.MaxAgeMillis, + } +} + +func protoExecutionFromModel(src *model.ExecutionRefs) *orchestratorv1.ExecutionRefs { + if src == nil { + return nil + } + return &orchestratorv1.ExecutionRefs{ + DebitEntryRef: src.DebitEntryRef, + CreditEntryRef: src.CreditEntryRef, + FxEntryRef: src.FXEntryRef, + ChainTransferRef: src.ChainTransferRef, + } +} + +func modelQuoteToProto(src *model.PaymentQuoteSnapshot) *orchestratorv1.PaymentQuote { + if src == nil { + return nil + } + return &orchestratorv1.PaymentQuote{ + DebitAmount: cloneMoney(src.DebitAmount), + ExpectedSettlementAmount: cloneMoney(src.ExpectedSettlementAmount), + ExpectedFeeTotal: cloneMoney(src.ExpectedFeeTotal), + FeeLines: cloneFeeLines(src.FeeLines), + FeeRules: cloneFeeRules(src.FeeRules), + FxQuote: cloneFXQuote(src.FXQuote), + NetworkFee: cloneNetworkEstimate(src.NetworkFee), + FeeQuoteToken: src.FeeQuoteToken, + } +} + +func filterFromProto(req *orchestratorv1.ListPaymentsRequest) *model.PaymentFilter { + if req == nil { + return &model.PaymentFilter{} + } + filter := &model.PaymentFilter{ + SourceRef: strings.TrimSpace(req.GetSourceRef()), + DestinationRef: strings.TrimSpace(req.GetDestinationRef()), + } + if req.GetPage() != nil { + filter.Cursor = strings.TrimSpace(req.GetPage().GetCursor()) + filter.Limit = req.GetPage().GetLimit() + } + if len(req.GetFilterStates()) > 0 { + filter.States = make([]model.PaymentState, 0, len(req.GetFilterStates())) + for _, st := range req.GetFilterStates() { + filter.States = append(filter.States, modelStateFromProto(st)) + } + } + return filter +} + +func protoKindFromModel(kind model.PaymentKind) orchestratorv1.PaymentKind { + switch kind { + case model.PaymentKindPayout: + return orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT + case model.PaymentKindInternalTransfer: + return orchestratorv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER + case model.PaymentKindFXConversion: + return orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION + default: + return orchestratorv1.PaymentKind_PAYMENT_KIND_UNSPECIFIED + } +} + +func modelKindFromProto(kind orchestratorv1.PaymentKind) model.PaymentKind { + switch kind { + case orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT: + return model.PaymentKindPayout + case orchestratorv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER: + return model.PaymentKindInternalTransfer + case orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION: + return model.PaymentKindFXConversion + default: + return model.PaymentKindUnspecified + } +} + +func protoStateFromModel(state model.PaymentState) orchestratorv1.PaymentState { + switch state { + case model.PaymentStateAccepted: + return orchestratorv1.PaymentState_PAYMENT_STATE_ACCEPTED + case model.PaymentStateFundsReserved: + return orchestratorv1.PaymentState_PAYMENT_STATE_FUNDS_RESERVED + case model.PaymentStateSubmitted: + return orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED + case model.PaymentStateSettled: + return orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED + case model.PaymentStateFailed: + return orchestratorv1.PaymentState_PAYMENT_STATE_FAILED + case model.PaymentStateCancelled: + return orchestratorv1.PaymentState_PAYMENT_STATE_CANCELLED + default: + return orchestratorv1.PaymentState_PAYMENT_STATE_UNSPECIFIED + } +} + +func modelStateFromProto(state orchestratorv1.PaymentState) model.PaymentState { + switch state { + case orchestratorv1.PaymentState_PAYMENT_STATE_ACCEPTED: + return model.PaymentStateAccepted + case orchestratorv1.PaymentState_PAYMENT_STATE_FUNDS_RESERVED: + return model.PaymentStateFundsReserved + case orchestratorv1.PaymentState_PAYMENT_STATE_SUBMITTED: + return model.PaymentStateSubmitted + case orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED: + return model.PaymentStateSettled + case orchestratorv1.PaymentState_PAYMENT_STATE_FAILED: + return model.PaymentStateFailed + case orchestratorv1.PaymentState_PAYMENT_STATE_CANCELLED: + return model.PaymentStateCancelled + default: + return model.PaymentStateUnspecified + } +} + +func protoFailureFromModel(code model.PaymentFailureCode) orchestratorv1.PaymentFailureCode { + switch code { + case model.PaymentFailureCodeBalance: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_BALANCE + case model.PaymentFailureCodeLedger: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_LEDGER + case model.PaymentFailureCodeFX: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FX + case model.PaymentFailureCodeChain: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_CHAIN + case model.PaymentFailureCodeFees: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FEES + case model.PaymentFailureCodePolicy: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_POLICY + default: + return orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_UNSPECIFIED + } +} + +func cloneAsset(asset *gatewayv1.Asset) *gatewayv1.Asset { + if asset == nil { + return nil + } + return &gatewayv1.Asset{ + Chain: asset.GetChain(), + TokenSymbol: asset.GetTokenSymbol(), + ContractAddress: asset.GetContractAddress(), + } +} + +func clonePair(pair *fxv1.CurrencyPair) *fxv1.CurrencyPair { + if pair == nil { + return nil + } + return &fxv1.CurrencyPair{ + Base: pair.GetBase(), + Quote: pair.GetQuote(), + } +} + +func cloneFXQuote(quote *oraclev1.Quote) *oraclev1.Quote { + if quote == nil { + return nil + } + if cloned, ok := proto.Clone(quote).(*oraclev1.Quote); ok { + return cloned + } + return nil +} + +func cloneNetworkEstimate(resp *gatewayv1.EstimateTransferFeeResponse) *gatewayv1.EstimateTransferFeeResponse { + if resp == nil { + return nil + } + if cloned, ok := proto.Clone(resp).(*gatewayv1.EstimateTransferFeeResponse); ok { + return cloned + } + return nil +} + +func protoFailureToModel(code orchestratorv1.PaymentFailureCode) model.PaymentFailureCode { + switch code { + case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_BALANCE: + return model.PaymentFailureCodeBalance + case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_LEDGER: + return model.PaymentFailureCodeLedger + case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FX: + return model.PaymentFailureCodeFX + case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_CHAIN: + return model.PaymentFailureCodeChain + case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_FEES: + return model.PaymentFailureCodeFees + case orchestratorv1.PaymentFailureCode_PAYMENT_FAILURE_CODE_POLICY: + return model.PaymentFailureCodePolicy + default: + return model.PaymentFailureCodeUnspecified + } +} + +func applyProtoPaymentToModel(src *orchestratorv1.Payment, dst *model.Payment) error { + if src == nil || dst == nil { + return merrors.InvalidArgument("payment payload is required") + } + dst.PaymentRef = strings.TrimSpace(src.GetPaymentRef()) + dst.IdempotencyKey = strings.TrimSpace(src.GetIdempotencyKey()) + dst.Intent = intentFromProto(src.GetIntent()) + dst.State = modelStateFromProto(src.GetState()) + dst.FailureCode = protoFailureToModel(src.GetFailureCode()) + dst.FailureReason = strings.TrimSpace(src.GetFailureReason()) + dst.Metadata = cloneMetadata(src.GetMetadata()) + dst.LastQuote = quoteSnapshotToModel(src.GetLastQuote()) + dst.Execution = executionFromProto(src.GetExecution()) + return nil +} + +func executionFromProto(src *orchestratorv1.ExecutionRefs) *model.ExecutionRefs { + if src == nil { + return nil + } + return &model.ExecutionRefs{ + DebitEntryRef: strings.TrimSpace(src.GetDebitEntryRef()), + CreditEntryRef: strings.TrimSpace(src.GetCreditEntryRef()), + FXEntryRef: strings.TrimSpace(src.GetFxEntryRef()), + ChainTransferRef: strings.TrimSpace(src.GetChainTransferRef()), + } +} + +func ensurePageRequest(req *orchestratorv1.ListPaymentsRequest) *paginationv1.CursorPageRequest { + if req == nil { + return &paginationv1.CursorPageRequest{} + } + if req.GetPage() == nil { + return &paginationv1.CursorPageRequest{} + } + return req.GetPage() +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/execution.go b/api/payments/orchestrator/internal/service/orchestrator/execution.go new file mode 100644 index 0000000..f8e9203 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/execution.go @@ -0,0 +1,495 @@ +package orchestrator + +import ( + "context" + "strings" + "time" + + oracleclient "github.com/tech/sendico/fx/oracle/client" + "github.com/tech/sendico/payments/orchestrator/storage" + "github.com/tech/sendico/payments/orchestrator/storage/model" + "github.com/tech/sendico/pkg/merrors" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" + "google.golang.org/protobuf/types/known/timestamppb" +) + +func (s *Service) buildPaymentQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.PaymentQuote, error) { + intent := req.GetIntent() + amount := intent.GetAmount() + baseAmount := cloneMoney(amount) + feeQuote, err := s.quoteFees(ctx, orgRef, req) + if err != nil { + return nil, err + } + feeTotal := extractFeeTotal(feeQuote.GetLines(), amount.GetCurrency()) + + var networkFee *gatewayv1.EstimateTransferFeeResponse + if shouldEstimateNetworkFee(intent) { + networkFee, err = s.estimateNetworkFee(ctx, intent) + if err != nil { + return nil, err + } + } + + var fxQuote *oraclev1.Quote + if shouldRequestFX(intent) { + fxQuote, err = s.requestFXQuote(ctx, orgRef, req) + if err != nil { + return nil, err + } + } + + debitAmount, settlementAmount := computeAggregates(baseAmount, feeTotal, networkFee) + + return &orchestratorv1.PaymentQuote{ + DebitAmount: debitAmount, + ExpectedSettlementAmount: settlementAmount, + ExpectedFeeTotal: feeTotal, + FeeLines: cloneFeeLines(feeQuote.GetLines()), + FeeRules: cloneFeeRules(feeQuote.GetApplied()), + FxQuote: fxQuote, + NetworkFee: networkFee, + FeeQuoteToken: feeQuote.GetFeeQuoteToken(), + }, nil +} + +func (s *Service) quoteFees(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*feesv1.PrecomputeFeesResponse, error) { + if !s.fees.available() { + return &feesv1.PrecomputeFeesResponse{}, nil + } + intent := req.GetIntent() + feeIntent := &feesv1.Intent{ + Trigger: triggerFromKind(intent.GetKind(), intent.GetRequiresFx()), + BaseAmount: cloneMoney(intent.GetAmount()), + BookedAt: timestamppb.New(s.clock.Now()), + OriginType: "payments.orchestrator.quote", + OriginRef: strings.TrimSpace(req.GetIdempotencyKey()), + Attributes: cloneMetadata(intent.GetAttributes()), + } + timeout := req.GetMeta().GetTrace() + ctxTimeout, cancel := s.withTimeout(ctx, s.fees.timeout) + defer cancel() + resp, err := s.fees.client.PrecomputeFees(ctxTimeout, &feesv1.PrecomputeFeesRequest{ + Meta: &feesv1.RequestMeta{ + OrganizationRef: orgRef, + Trace: timeout, + }, + Intent: feeIntent, + TtlMs: defaultFeeQuoteTTLMillis, + }) + if err != nil { + s.logger.Error("fees precompute failed", zap.Error(err)) + return nil, merrors.Internal("fees_precompute_failed") + } + return resp, nil +} + +func (s *Service) estimateNetworkFee(ctx context.Context, intent *orchestratorv1.PaymentIntent) (*gatewayv1.EstimateTransferFeeResponse, error) { + if !s.gateway.available() { + return nil, nil + } + + req := &gatewayv1.EstimateTransferFeeRequest{ + Amount: cloneMoney(intent.GetAmount()), + } + if src := intent.GetSource().GetManagedWallet(); src != nil { + req.SourceWalletRef = strings.TrimSpace(src.GetManagedWalletRef()) + } + if dst := intent.GetDestination().GetManagedWallet(); dst != nil { + req.Destination = &gatewayv1.TransferDestination{ + Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(dst.GetManagedWalletRef())}, + } + } + if dst := intent.GetDestination().GetExternalChain(); dst != nil { + req.Destination = &gatewayv1.TransferDestination{ + Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(dst.GetAddress())}, + Memo: strings.TrimSpace(dst.GetMemo()), + } + req.Asset = dst.GetAsset() + } + if req.Asset == nil { + if src := intent.GetSource().GetManagedWallet(); src != nil { + req.Asset = src.GetAsset() + } + } + + resp, err := s.gateway.client.EstimateTransferFee(ctx, req) + if err != nil { + s.logger.Error("chain gateway fee estimation failed", zap.Error(err)) + return nil, merrors.Internal("chain_gateway_fee_estimation_failed") + } + return resp, nil +} + +func (s *Service) requestFXQuote(ctx context.Context, orgRef string, req *orchestratorv1.QuotePaymentRequest) (*oraclev1.Quote, error) { + if !s.oracle.available() { + return nil, nil + } + intent := req.GetIntent() + meta := req.GetMeta() + fxIntent := intent.GetFx() + if fxIntent == nil { + return nil, nil + } + + ttl := fxIntent.GetTtlMs() + if ttl <= 0 { + ttl = defaultOracleTTLMillis + } + + params := oracleclient.GetQuoteParams{ + Meta: oracleclient.RequestMeta{ + OrganizationRef: orgRef, + Trace: meta.GetTrace(), + }, + Pair: fxIntent.GetPair(), + Side: fxIntent.GetSide(), + Firm: fxIntent.GetFirm(), + TTL: time.Duration(ttl) * time.Millisecond, + PreferredProvider: strings.TrimSpace(fxIntent.GetPreferredProvider()), + } + + if fxIntent.GetMaxAgeMs() > 0 { + params.MaxAge = time.Duration(fxIntent.GetMaxAgeMs()) * time.Millisecond + } + + if amount := intent.GetAmount(); amount != nil { + params.BaseAmount = cloneMoney(amount) + } + + quote, err := s.oracle.client.GetQuote(ctx, params) + if err != nil { + s.logger.Error("fx oracle quote failed", zap.Error(err)) + return nil, merrors.Internal("fx_quote_failed") + } + return quoteToProto(quote), nil +} + +func (s *Service) executePayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, quote *orchestratorv1.PaymentQuote) error { + if store == nil { + return errStorageUnavailable + } + + charges := ledgerChargesFromFeeLines(quote.GetFeeLines()) + ledgerNeeded := requiresLedger(payment) + chainNeeded := requiresChain(payment) + + exec := payment.Execution + if exec == nil { + exec = &model.ExecutionRefs{} + } + + if ledgerNeeded { + if !s.ledger.available() { + return s.failPayment(ctx, store, payment, model.PaymentFailureCodeLedger, "ledger_client_unavailable", merrors.Internal("ledger_client_unavailable")) + } + if err := s.performLedgerOperation(ctx, payment, quote, charges); err != nil { + return s.failPayment(ctx, store, payment, model.PaymentFailureCodeLedger, strings.TrimSpace(err.Error()), err) + } + payment.State = model.PaymentStateFundsReserved + if err := s.persistPayment(ctx, store, payment); err != nil { + return err + } + } + + if chainNeeded { + if !s.gateway.available() { + return s.failPayment(ctx, store, payment, model.PaymentFailureCodeChain, "chain_client_unavailable", merrors.Internal("chain_client_unavailable")) + } + resp, err := s.submitChainTransfer(ctx, payment, quote) + if err != nil { + return s.failPayment(ctx, store, payment, model.PaymentFailureCodeChain, strings.TrimSpace(err.Error()), err) + } + exec = payment.Execution + if exec == nil { + exec = &model.ExecutionRefs{} + } + if resp != nil && resp.GetTransfer() != nil { + exec.ChainTransferRef = strings.TrimSpace(resp.GetTransfer().GetTransferRef()) + } + payment.Execution = exec + payment.State = model.PaymentStateSubmitted + if err := s.persistPayment(ctx, store, payment); err != nil { + return err + } + return nil + } + + payment.State = model.PaymentStateSettled + return s.persistPayment(ctx, store, payment) +} + +func (s *Service) performLedgerOperation(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine) error { + intent := payment.Intent + if payment.OrganizationRef == primitive.NilObjectID { + return merrors.InvalidArgument("ledger: organization_ref is required") + } + + amount := cloneMoney(intent.Amount) + if amount == nil { + return merrors.InvalidArgument("ledger: amount is required") + } + + description := paymentDescription(payment) + metadata := cloneMetadata(payment.Metadata) + exec := payment.Execution + if exec == nil { + exec = &model.ExecutionRefs{} + } + + switch intent.Kind { + case model.PaymentKindFXConversion: + if err := s.applyFX(ctx, payment, quote, charges, description, metadata, exec); err != nil { + return err + } + case model.PaymentKindInternalTransfer, model.PaymentKindPayout, model.PaymentKindUnspecified: + from, to, err := resolveLedgerAccounts(intent) + if err != nil { + return err + } + req := &ledgerv1.TransferRequest{ + IdempotencyKey: payment.IdempotencyKey, + OrganizationRef: payment.OrganizationRef.Hex(), + FromLedgerAccountRef: from, + ToLedgerAccountRef: to, + Money: amount, + Description: description, + Charges: charges, + Metadata: metadata, + } + resp, err := s.ledger.client.TransferInternal(ctx, req) + if err != nil { + return err + } + exec.DebitEntryRef = strings.TrimSpace(resp.GetJournalEntryRef()) + payment.Execution = exec + default: + return merrors.InvalidArgument("ledger: unsupported payment kind") + } + + return nil +} + +func (s *Service) applyFX(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote, charges []*ledgerv1.PostingLine, description string, metadata map[string]string, exec *model.ExecutionRefs) error { + intent := payment.Intent + source := intent.Source.Ledger + destination := intent.Destination.Ledger + if source == nil || destination == nil { + return merrors.InvalidArgument("ledger: fx conversion requires ledger source and destination") + } + fq := quote.GetFxQuote() + if fq == nil { + return merrors.InvalidArgument("ledger: fx quote missing") + } + fromMoney := cloneMoney(fq.GetBaseAmount()) + if fromMoney == nil { + fromMoney = cloneMoney(intent.Amount) + } + toMoney := cloneMoney(fq.GetQuoteAmount()) + if toMoney == nil { + toMoney = cloneMoney(quote.GetExpectedSettlementAmount()) + } + rate := "" + if fq.GetPrice() != nil { + rate = fq.GetPrice().GetValue() + } + req := &ledgerv1.FXRequest{ + IdempotencyKey: payment.IdempotencyKey, + OrganizationRef: payment.OrganizationRef.Hex(), + FromLedgerAccountRef: strings.TrimSpace(source.LedgerAccountRef), + ToLedgerAccountRef: strings.TrimSpace(destination.LedgerAccountRef), + FromMoney: fromMoney, + ToMoney: toMoney, + Rate: rate, + Description: description, + Charges: charges, + Metadata: metadata, + } + resp, err := s.ledger.client.ApplyFXWithCharges(ctx, req) + if err != nil { + return err + } + exec.FXEntryRef = strings.TrimSpace(resp.GetJournalEntryRef()) + payment.Execution = exec + return nil +} + +func (s *Service) submitChainTransfer(ctx context.Context, payment *model.Payment, quote *orchestratorv1.PaymentQuote) (*gatewayv1.SubmitTransferResponse, error) { + intent := payment.Intent + source := intent.Source.ManagedWallet + destination := intent.Destination + if source == nil || strings.TrimSpace(source.ManagedWalletRef) == "" { + return nil, merrors.InvalidArgument("chain: source managed wallet is required") + } + dest, err := toGatewayDestination(destination) + if err != nil { + return nil, err + } + amount := cloneMoney(intent.Amount) + if amount == nil { + return nil, merrors.InvalidArgument("chain: amount is required") + } + fees := feeBreakdownFromQuote(quote) + req := &gatewayv1.SubmitTransferRequest{ + IdempotencyKey: payment.IdempotencyKey, + OrganizationRef: payment.OrganizationRef.Hex(), + SourceWalletRef: strings.TrimSpace(source.ManagedWalletRef), + Destination: dest, + Amount: amount, + Fees: fees, + Metadata: cloneMetadata(payment.Metadata), + ClientReference: payment.PaymentRef, + } + return s.gateway.client.SubmitTransfer(ctx, req) +} + +func (s *Service) persistPayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment) error { + if store == nil { + return errStorageUnavailable + } + return store.Update(ctx, payment) +} + +func (s *Service) failPayment(ctx context.Context, store storage.PaymentsStore, payment *model.Payment, code model.PaymentFailureCode, reason string, err error) error { + payment.State = model.PaymentStateFailed + payment.FailureCode = code + payment.FailureReason = strings.TrimSpace(reason) + if store != nil { + if updateErr := store.Update(ctx, payment); updateErr != nil { + s.logger.Error("failed to persist payment failure", zap.Error(updateErr), zap.String("payment_ref", payment.PaymentRef)) + } + } + if err != nil { + return err + } + return merrors.Internal(reason) +} + +func resolveLedgerAccounts(intent model.PaymentIntent) (string, string, error) { + source := intent.Source.Ledger + destination := intent.Destination.Ledger + if source == nil || strings.TrimSpace(source.LedgerAccountRef) == "" { + return "", "", merrors.InvalidArgument("ledger: source account is required") + } + to := "" + if destination != nil && strings.TrimSpace(destination.LedgerAccountRef) != "" { + to = strings.TrimSpace(destination.LedgerAccountRef) + } else if strings.TrimSpace(source.ContraLedgerAccountRef) != "" { + to = strings.TrimSpace(source.ContraLedgerAccountRef) + } + if to == "" { + return "", "", merrors.InvalidArgument("ledger: destination account is required") + } + return strings.TrimSpace(source.LedgerAccountRef), to, nil +} + +func paymentDescription(payment *model.Payment) string { + if payment == nil { + return "" + } + if val := strings.TrimSpace(payment.Intent.Attributes["description"]); val != "" { + return val + } + if payment.Metadata != nil { + if val := strings.TrimSpace(payment.Metadata["description"]); val != "" { + return val + } + } + return payment.PaymentRef +} + +func requiresLedger(payment *model.Payment) bool { + if payment == nil { + return false + } + if payment.Intent.Kind == model.PaymentKindFXConversion { + return true + } + return hasLedgerEndpoint(payment.Intent.Source) || hasLedgerEndpoint(payment.Intent.Destination) +} + +func requiresChain(payment *model.Payment) bool { + if payment == nil { + return false + } + if !hasManagedWallet(payment.Intent.Source) { + return false + } + switch payment.Intent.Destination.Type { + case model.EndpointTypeManagedWallet, model.EndpointTypeExternalChain: + return true + default: + return false + } +} + +func hasLedgerEndpoint(endpoint model.PaymentEndpoint) bool { + return endpoint.Type == model.EndpointTypeLedger && endpoint.Ledger != nil && strings.TrimSpace(endpoint.Ledger.LedgerAccountRef) != "" +} + +func hasManagedWallet(endpoint model.PaymentEndpoint) bool { + return endpoint.Type == model.EndpointTypeManagedWallet && endpoint.ManagedWallet != nil && strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) != "" +} + +func toGatewayDestination(endpoint model.PaymentEndpoint) (*gatewayv1.TransferDestination, error) { + switch endpoint.Type { + case model.EndpointTypeManagedWallet: + if endpoint.ManagedWallet == nil || strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef) == "" { + return nil, merrors.InvalidArgument("chain: destination managed wallet is required") + } + return &gatewayv1.TransferDestination{ + Destination: &gatewayv1.TransferDestination_ManagedWalletRef{ManagedWalletRef: strings.TrimSpace(endpoint.ManagedWallet.ManagedWalletRef)}, + }, nil + case model.EndpointTypeExternalChain: + if endpoint.ExternalChain == nil || strings.TrimSpace(endpoint.ExternalChain.Address) == "" { + return nil, merrors.InvalidArgument("chain: external address is required") + } + return &gatewayv1.TransferDestination{ + Destination: &gatewayv1.TransferDestination_ExternalAddress{ExternalAddress: strings.TrimSpace(endpoint.ExternalChain.Address)}, + Memo: strings.TrimSpace(endpoint.ExternalChain.Memo), + }, nil + default: + return nil, merrors.InvalidArgument("chain: unsupported destination type") + } +} + +func applyTransferStatus(event *gatewayv1.TransferStatusChangedEvent, payment *model.Payment) { + if payment.Execution == nil { + payment.Execution = &model.ExecutionRefs{} + } + if event == nil || event.GetTransfer() == nil { + return + } + transfer := event.GetTransfer() + payment.Execution.ChainTransferRef = strings.TrimSpace(transfer.GetTransferRef()) + reason := strings.TrimSpace(event.GetReason()) + if reason == "" { + reason = strings.TrimSpace(transfer.GetFailureReason()) + } + switch transfer.GetStatus() { + case gatewayv1.TransferStatus_TRANSFER_CONFIRMED: + payment.State = model.PaymentStateSettled + payment.FailureCode = model.PaymentFailureCodeUnspecified + payment.FailureReason = "" + case gatewayv1.TransferStatus_TRANSFER_FAILED: + payment.State = model.PaymentStateFailed + payment.FailureCode = model.PaymentFailureCodeChain + payment.FailureReason = reason + case gatewayv1.TransferStatus_TRANSFER_CANCELLED: + payment.State = model.PaymentStateCancelled + payment.FailureCode = model.PaymentFailureCodePolicy + payment.FailureReason = reason + case gatewayv1.TransferStatus_TRANSFER_SIGNING, + gatewayv1.TransferStatus_TRANSFER_PENDING, + gatewayv1.TransferStatus_TRANSFER_SUBMITTED: + payment.State = model.PaymentStateSubmitted + default: + // retain previous state + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/helpers.go b/api/payments/orchestrator/internal/service/orchestrator/helpers.go new file mode 100644 index 0000000..895e32f --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/helpers.go @@ -0,0 +1,295 @@ +package orchestrator + +import ( + "strings" + + oracleclient "github.com/tech/sendico/fx/oracle/client" + "github.com/tech/sendico/pkg/merrors" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "github.com/shopspring/decimal" + "google.golang.org/protobuf/proto" + + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" + accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" +) + +func cloneMoney(input *moneyv1.Money) *moneyv1.Money { + if input == nil { + return nil + } + return &moneyv1.Money{ + Currency: input.GetCurrency(), + Amount: input.GetAmount(), + } +} + +func cloneMetadata(input map[string]string) map[string]string { + if len(input) == 0 { + return nil + } + clone := make(map[string]string, len(input)) + for k, v := range input { + clone[k] = v + } + return clone +} + +func cloneFeeLines(lines []*feesv1.DerivedPostingLine) []*feesv1.DerivedPostingLine { + if len(lines) == 0 { + return nil + } + out := make([]*feesv1.DerivedPostingLine, 0, len(lines)) + for _, line := range lines { + if line == nil { + continue + } + if cloned, ok := proto.Clone(line).(*feesv1.DerivedPostingLine); ok { + out = append(out, cloned) + } + } + if len(out) == 0 { + return nil + } + return out +} + +func cloneFeeRules(rules []*feesv1.AppliedRule) []*feesv1.AppliedRule { + if len(rules) == 0 { + return nil + } + out := make([]*feesv1.AppliedRule, 0, len(rules)) + for _, rule := range rules { + if rule == nil { + continue + } + if cloned, ok := proto.Clone(rule).(*feesv1.AppliedRule); ok { + out = append(out, cloned) + } + } + if len(out) == 0 { + return nil + } + return out +} + +func extractFeeTotal(lines []*feesv1.DerivedPostingLine, currency string) *moneyv1.Money { + if len(lines) == 0 || currency == "" { + return nil + } + total := decimal.Zero + for _, line := range lines { + if line == nil || line.GetMoney() == nil { + continue + } + if !strings.EqualFold(line.GetMoney().GetCurrency(), currency) { + continue + } + amount, err := decimal.NewFromString(line.GetMoney().GetAmount()) + if err != nil { + continue + } + switch line.GetSide() { + case accountingv1.EntrySide_ENTRY_SIDE_CREDIT: + total = total.Sub(amount.Abs()) + default: + total = total.Add(amount.Abs()) + } + } + if total.IsZero() { + return nil + } + return &moneyv1.Money{ + Currency: currency, + Amount: total.String(), + } +} + +func computeAggregates(base, fee *moneyv1.Money, network *gatewayv1.EstimateTransferFeeResponse) (*moneyv1.Money, *moneyv1.Money) { + if base == nil { + return nil, nil + } + baseDecimal, err := decimalFromMoney(base) + if err != nil { + return cloneMoney(base), cloneMoney(base) + } + debit := baseDecimal + settlement := baseDecimal + + if feeDecimal, err := decimalFromMoneyMatching(base, fee); err == nil && feeDecimal != nil { + debit = debit.Add(*feeDecimal) + settlement = settlement.Sub(*feeDecimal) + } + + if network != nil && network.GetNetworkFee() != nil { + if networkDecimal, err := decimalFromMoneyMatching(base, network.GetNetworkFee()); err == nil && networkDecimal != nil { + debit = debit.Add(*networkDecimal) + settlement = settlement.Sub(*networkDecimal) + } + } + + return makeMoney(base.GetCurrency(), debit), makeMoney(base.GetCurrency(), settlement) +} + +func decimalFromMoney(m *moneyv1.Money) (decimal.Decimal, error) { + if m == nil { + return decimal.Zero, nil + } + return decimal.NewFromString(m.GetAmount()) +} + +func decimalFromMoneyMatching(reference, candidate *moneyv1.Money) (*decimal.Decimal, error) { + if reference == nil || candidate == nil { + return nil, nil + } + if !strings.EqualFold(reference.GetCurrency(), candidate.GetCurrency()) { + return nil, nil + } + value, err := decimal.NewFromString(candidate.GetAmount()) + if err != nil { + return nil, err + } + return &value, nil +} + +func makeMoney(currency string, value decimal.Decimal) *moneyv1.Money { + return &moneyv1.Money{ + Currency: currency, + Amount: value.String(), + } +} + +func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote { + if src == nil { + return nil + } + return &oraclev1.Quote{ + QuoteRef: src.QuoteRef, + Pair: src.Pair, + Side: src.Side, + Price: &moneyv1.Decimal{Value: src.Price}, + BaseAmount: cloneMoney(src.BaseAmount), + QuoteAmount: cloneMoney(src.QuoteAmount), + ExpiresAtUnixMs: src.ExpiresAt.UnixMilli(), + Provider: src.Provider, + RateRef: src.RateRef, + Firm: src.Firm, + } +} + +func ledgerChargesFromFeeLines(lines []*feesv1.DerivedPostingLine) []*ledgerv1.PostingLine { + if len(lines) == 0 { + return nil + } + charges := make([]*ledgerv1.PostingLine, 0, len(lines)) + for _, line := range lines { + if line == nil || strings.TrimSpace(line.GetLedgerAccountRef()) == "" { + continue + } + money := cloneMoney(line.GetMoney()) + if money == nil { + continue + } + charges = append(charges, &ledgerv1.PostingLine{ + LedgerAccountRef: strings.TrimSpace(line.GetLedgerAccountRef()), + Money: money, + LineType: ledgerLineTypeFromAccounting(line.GetLineType()), + }) + } + if len(charges) == 0 { + return nil + } + return charges +} + +func ledgerLineTypeFromAccounting(lineType accountingv1.PostingLineType) ledgerv1.LineType { + switch lineType { + case accountingv1.PostingLineType_POSTING_LINE_SPREAD: + return ledgerv1.LineType_LINE_SPREAD + case accountingv1.PostingLineType_POSTING_LINE_REVERSAL: + return ledgerv1.LineType_LINE_REVERSAL + case accountingv1.PostingLineType_POSTING_LINE_FEE, + accountingv1.PostingLineType_POSTING_LINE_TAX: + return ledgerv1.LineType_LINE_FEE + default: + return ledgerv1.LineType_LINE_MAIN + } +} + +func feeBreakdownFromQuote(quote *orchestratorv1.PaymentQuote) []*gatewayv1.ServiceFeeBreakdown { + if quote == nil { + return nil + } + lines := quote.GetFeeLines() + breakdown := make([]*gatewayv1.ServiceFeeBreakdown, 0, len(lines)+1) + for _, line := range lines { + if line == nil { + continue + } + amount := cloneMoney(line.GetMoney()) + if amount == nil { + continue + } + code := strings.TrimSpace(line.GetMeta()["fee_code"]) + if code == "" { + code = strings.TrimSpace(line.GetMeta()["fee_rule_id"]) + } + if code == "" { + code = line.GetLineType().String() + } + desc := strings.TrimSpace(line.GetMeta()["description"]) + breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{ + FeeCode: code, + Amount: amount, + Description: desc, + }) + } + if quote.GetNetworkFee() != nil && quote.GetNetworkFee().GetNetworkFee() != nil { + networkAmount := cloneMoney(quote.GetNetworkFee().GetNetworkFee()) + if networkAmount != nil { + breakdown = append(breakdown, &gatewayv1.ServiceFeeBreakdown{ + FeeCode: "network_fee", + Amount: networkAmount, + Description: strings.TrimSpace(quote.GetNetworkFee().GetEstimationContext()), + }) + } + } + if len(breakdown) == 0 { + return nil + } + return breakdown +} + +func moneyEquals(a, b *moneyv1.Money) bool { + if a == nil || b == nil { + return false + } + if !strings.EqualFold(a.GetCurrency(), b.GetCurrency()) { + return false + } + return strings.TrimSpace(a.GetAmount()) == strings.TrimSpace(b.GetAmount()) +} + +func conversionAmountFromMetadata(meta map[string]string, fx *orchestratorv1.FXIntent) (*moneyv1.Money, error) { + if meta == nil { + meta = map[string]string{} + } + amount := strings.TrimSpace(meta["amount"]) + if amount == "" { + return nil, merrors.InvalidArgument("conversion amount metadata is required") + } + currency := strings.TrimSpace(meta["currency"]) + if currency == "" && fx != nil && fx.GetPair() != nil { + currency = strings.TrimSpace(fx.GetPair().GetBase()) + } + if currency == "" { + return nil, merrors.InvalidArgument("conversion currency metadata is required") + } + return &moneyv1.Money{ + Currency: currency, + Amount: amount, + }, nil +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/internal_helpers.go b/api/payments/orchestrator/internal/service/orchestrator/internal_helpers.go new file mode 100644 index 0000000..3401eb9 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/internal_helpers.go @@ -0,0 +1,71 @@ +package orchestrator + +import ( + "context" + "time" + + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/mservice" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" +) + +func (s *Service) ensureRepository(ctx context.Context) error { + if s.storage == nil { + return errStorageUnavailable + } + return s.storage.Ping(ctx) +} + +func (s *Service) withTimeout(ctx context.Context, d time.Duration) (context.Context, context.CancelFunc) { + if d <= 0 { + return context.WithCancel(ctx) + } + return context.WithTimeout(ctx, d) +} + +func executeUnary[TReq any, TResp any](ctx context.Context, svc *Service, method string, handler func(context.Context, *TReq) gsresponse.Responder[TResp], req *TReq) (*TResp, error) { + start := svc.clock.Now() + resp, err := gsresponse.Unary(svc.logger, mservice.PaymentOrchestrator, handler)(ctx, req) + observeRPC(method, err, svc.clock.Now().Sub(start)) + return resp, err +} + +func triggerFromKind(kind orchestratorv1.PaymentKind, requiresFX bool) feesv1.Trigger { + switch kind { + case orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT: + return feesv1.Trigger_TRIGGER_PAYOUT + case orchestratorv1.PaymentKind_PAYMENT_KIND_INTERNAL_TRANSFER: + return feesv1.Trigger_TRIGGER_CAPTURE + case orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION: + return feesv1.Trigger_TRIGGER_FX_CONVERSION + default: + if requiresFX { + return feesv1.Trigger_TRIGGER_FX_CONVERSION + } + return feesv1.Trigger_TRIGGER_UNSPECIFIED + } +} + +func shouldEstimateNetworkFee(intent *orchestratorv1.PaymentIntent) bool { + if intent == nil { + return false + } + if intent.GetKind() == orchestratorv1.PaymentKind_PAYMENT_KIND_PAYOUT { + return true + } + if intent.GetDestination().GetManagedWallet() != nil || intent.GetDestination().GetExternalChain() != nil { + return true + } + return false +} + +func shouldRequestFX(intent *orchestratorv1.PaymentIntent) bool { + if intent == nil { + return false + } + if intent.GetRequiresFx() { + return true + } + return intent.GetFx() != nil && intent.GetFx().GetPair() != nil +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/metrics.go b/api/payments/orchestrator/internal/service/orchestrator/metrics.go new file mode 100644 index 0000000..417eb90 --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/metrics.go @@ -0,0 +1,65 @@ +package orchestrator + +import ( + "errors" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "github.com/prometheus/client_golang/prometheus/promauto" + "github.com/tech/sendico/pkg/merrors" +) + +var ( + metricsOnce sync.Once + + rpcLatency *prometheus.HistogramVec + rpcStatus *prometheus.CounterVec +) + +func initMetrics() { + metricsOnce.Do(func() { + rpcLatency = promauto.NewHistogramVec(prometheus.HistogramOpts{ + Namespace: "sendico", + Subsystem: "payment_orchestrator", + Name: "rpc_latency_seconds", + Help: "Latency distribution for payment orchestrator RPC handlers.", + Buckets: prometheus.DefBuckets, + }, []string{"method"}) + + rpcStatus = promauto.NewCounterVec(prometheus.CounterOpts{ + Namespace: "sendico", + Subsystem: "payment_orchestrator", + Name: "rpc_requests_total", + Help: "Total number of RPC invocations grouped by method and status.", + }, []string{"method", "status"}) + }) +} + +func observeRPC(method string, err error, duration time.Duration) { + if rpcLatency != nil { + rpcLatency.WithLabelValues(method).Observe(duration.Seconds()) + } + if rpcStatus != nil { + rpcStatus.WithLabelValues(method, statusLabel(err)).Inc() + } +} + +func statusLabel(err error) string { + switch { + case err == nil: + return "ok" + case errors.Is(err, merrors.ErrInvalidArg): + return "invalid_argument" + case errors.Is(err, merrors.ErrNoData): + return "not_found" + case errors.Is(err, merrors.ErrDataConflict): + return "conflict" + case errors.Is(err, merrors.ErrAccessDenied): + return "denied" + case errors.Is(err, merrors.ErrInternal): + return "internal" + default: + return "error" + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/options.go b/api/payments/orchestrator/internal/service/orchestrator/options.go new file mode 100644 index 0000000..63120af --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/options.go @@ -0,0 +1,87 @@ +package orchestrator + +import ( + "time" + + chainclient "github.com/tech/sendico/chain/gateway/client" + oracleclient "github.com/tech/sendico/fx/oracle/client" + ledgerclient "github.com/tech/sendico/ledger/client" + clockpkg "github.com/tech/sendico/pkg/clock" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" +) + +// Option configures service dependencies. +type Option func(*Service) + +type feesDependency struct { + client feesv1.FeeEngineClient + timeout time.Duration +} + +func (f feesDependency) available() bool { + return f.client != nil +} + +type ledgerDependency struct { + client ledgerclient.Client +} + +func (l ledgerDependency) available() bool { + return l.client != nil +} + +type gatewayDependency struct { + client chainclient.Client +} + +func (g gatewayDependency) available() bool { + return g.client != nil +} + +type oracleDependency struct { + client oracleclient.Client +} + +func (o oracleDependency) available() bool { + return o.client != nil +} + +// WithFeeEngine wires the fee engine client. +func WithFeeEngine(client feesv1.FeeEngineClient, timeout time.Duration) Option { + return func(s *Service) { + s.fees = feesDependency{ + client: client, + timeout: timeout, + } + } +} + +// WithLedgerClient wires the ledger client. +func WithLedgerClient(client ledgerclient.Client) Option { + return func(s *Service) { + s.ledger = ledgerDependency{client: client} + } +} + +// WithChainGatewayClient wires the chain gateway client. +func WithChainGatewayClient(client chainclient.Client) Option { + return func(s *Service) { + s.gateway = gatewayDependency{client: client} + } +} + +// WithOracleClient wires the FX oracle client. +func WithOracleClient(client oracleclient.Client) Option { + return func(s *Service) { + s.oracle = oracleDependency{client: client} + } +} + +// WithClock overrides the default clock. +func WithClock(clock clockpkg.Clock) Option { + return func(s *Service) { + if clock != nil { + s.clock = clock + } + } +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/service.go b/api/payments/orchestrator/internal/service/orchestrator/service.go new file mode 100644 index 0000000..3dce79a --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/service.go @@ -0,0 +1,504 @@ +package orchestrator + +import ( + "context" + "strings" + + "github.com/tech/sendico/payments/orchestrator/storage" + "github.com/tech/sendico/payments/orchestrator/storage/model" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + clockpkg "github.com/tech/sendico/pkg/clock" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + paginationv1 "github.com/tech/sendico/pkg/proto/common/pagination/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "go.mongodb.org/mongo-driver/bson/primitive" + "google.golang.org/grpc" +) + +type serviceError string + +func (e serviceError) Error() string { + return string(e) +} + +const ( + defaultFeeQuoteTTLMillis int64 = 120000 + defaultOracleTTLMillis int64 = 60000 +) + +var ( + errStorageUnavailable = serviceError("payments.orchestrator: storage not initialised") +) + +// Service orchestrates payments across ledger, billing, FX, and chain domains. +type Service struct { + logger mlogger.Logger + storage storage.Repository + clock clockpkg.Clock + + fees feesDependency + ledger ledgerDependency + gateway gatewayDependency + oracle oracleDependency + + orchestratorv1.UnimplementedPaymentOrchestratorServer +} + +// NewService constructs a payment orchestrator service. +func NewService(logger mlogger.Logger, repo storage.Repository, opts ...Option) *Service { + svc := &Service{ + logger: logger.Named("payment_orchestrator"), + storage: repo, + clock: clockpkg.NewSystem(), + } + + initMetrics() + + for _, opt := range opts { + if opt != nil { + opt(svc) + } + } + + if svc.clock == nil { + svc.clock = clockpkg.NewSystem() + } + + return svc +} + +// Register attaches the service to the supplied gRPC router. +func (s *Service) Register(router routers.GRPC) error { + return router.Register(func(reg grpc.ServiceRegistrar) { + orchestratorv1.RegisterPaymentOrchestratorServer(reg, s) + }) +} + +// QuotePayment aggregates downstream quotes. +func (s *Service) QuotePayment(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) (*orchestratorv1.QuotePaymentResponse, error) { + return executeUnary(ctx, s, "QuotePayment", s.quotePaymentHandler, req) +} + +// InitiatePayment captures a payment intent and reserves funds orchestration. +func (s *Service) InitiatePayment(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) (*orchestratorv1.InitiatePaymentResponse, error) { + return executeUnary(ctx, s, "InitiatePayment", s.initiatePaymentHandler, req) +} + +// CancelPayment attempts to cancel an in-flight payment. +func (s *Service) CancelPayment(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) (*orchestratorv1.CancelPaymentResponse, error) { + return executeUnary(ctx, s, "CancelPayment", s.cancelPaymentHandler, req) +} + +// GetPayment returns a stored payment record. +func (s *Service) GetPayment(ctx context.Context, req *orchestratorv1.GetPaymentRequest) (*orchestratorv1.GetPaymentResponse, error) { + return executeUnary(ctx, s, "GetPayment", s.getPaymentHandler, req) +} + +// ListPayments lists stored payment records. +func (s *Service) ListPayments(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) (*orchestratorv1.ListPaymentsResponse, error) { + return executeUnary(ctx, s, "ListPayments", s.listPaymentsHandler, req) +} + +// InitiateConversion orchestrates standalone FX conversions. +func (s *Service) InitiateConversion(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) (*orchestratorv1.InitiateConversionResponse, error) { + return executeUnary(ctx, s, "InitiateConversion", s.initiateConversionHandler, req) +} + +// ProcessTransferUpdate reconciles chain events back into payment state. +func (s *Service) ProcessTransferUpdate(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) (*orchestratorv1.ProcessTransferUpdateResponse, error) { + return executeUnary(ctx, s, "ProcessTransferUpdate", s.processTransferUpdateHandler, req) +} + +// ProcessDepositObserved reconciles deposit events to ledger. +func (s *Service) ProcessDepositObserved(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) (*orchestratorv1.ProcessDepositObservedResponse, error) { + return executeUnary(ctx, s, "ProcessDepositObserved", s.processDepositObservedHandler, req) +} + +func (s *Service) quotePaymentHandler(ctx context.Context, req *orchestratorv1.QuotePaymentRequest) gsresponse.Responder[orchestratorv1.QuotePaymentResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil { + return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) + } + meta := req.GetMeta() + if meta == nil { + return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("meta is required")) + } + orgRef := strings.TrimSpace(meta.GetOrganizationRef()) + if orgRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref is required")) + } + intent := req.GetIntent() + if intent == nil { + return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent is required")) + } + if intent.GetAmount() == nil { + return gsresponse.InvalidArgument[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent.amount is required")) + } + + quote, err := s.buildPaymentQuote(ctx, orgRef, req) + if err != nil { + return gsresponse.Auto[orchestratorv1.QuotePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + return gsresponse.Success(&orchestratorv1.QuotePaymentResponse{Quote: quote}) +} + +func (s *Service) initiatePaymentHandler(ctx context.Context, req *orchestratorv1.InitiatePaymentRequest) gsresponse.Responder[orchestratorv1.InitiatePaymentResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) + } + meta := req.GetMeta() + if meta == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("meta is required")) + } + orgRef := strings.TrimSpace(meta.GetOrganizationRef()) + if orgRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref is required")) + } + orgObjectID, parseErr := primitive.ObjectIDFromHex(orgRef) + if parseErr != nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref must be a valid objectID")) + } + intent := req.GetIntent() + if intent == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent is required")) + } + if intent.GetAmount() == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("intent.amount is required")) + } + idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey()) + if idempotencyKey == "" { + return gsresponse.InvalidArgument[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("idempotency_key is required")) + } + + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + + existing, err := store.GetByIdempotencyKey(ctx, orgObjectID, idempotencyKey) + if err == nil && existing != nil { + return gsresponse.Success(&orchestratorv1.InitiatePaymentResponse{ + Payment: toProtoPayment(existing), + }) + } + if err != nil && err != storage.ErrPaymentNotFound { + return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + quote := req.GetFeeQuoteToken() + var quoteSnapshot *orchestratorv1.PaymentQuote + if quote == "" { + quoteSnapshot, err = s.buildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{ + Meta: req.GetMeta(), + IdempotencyKey: req.GetIdempotencyKey(), + Intent: req.GetIntent(), + PreviewOnly: false, + }) + if err != nil { + return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + } else { + quoteSnapshot = &orchestratorv1.PaymentQuote{FeeQuoteToken: quote} + } + + entity := &model.Payment{} + entity.SetID(primitive.NewObjectID()) + entity.SetOrganizationRef(orgObjectID) + entity.PaymentRef = entity.GetID().Hex() + entity.IdempotencyKey = idempotencyKey + entity.State = model.PaymentStateAccepted + entity.Intent = intentFromProto(intent) + entity.Metadata = cloneMetadata(req.GetMetadata()) + entity.LastQuote = quoteSnapshotToModel(quoteSnapshot) + entity.Normalize() + + if err = store.Create(ctx, entity); err != nil { + if err == storage.ErrDuplicatePayment { + return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists")) + } + return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + if quoteSnapshot == nil { + quoteSnapshot = &orchestratorv1.PaymentQuote{} + } + + if err := s.executePayment(ctx, store, entity, quoteSnapshot); err != nil { + return gsresponse.Auto[orchestratorv1.InitiatePaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + return gsresponse.Success(&orchestratorv1.InitiatePaymentResponse{ + Payment: toProtoPayment(entity), + }) +} + +func (s *Service) cancelPaymentHandler(ctx context.Context, req *orchestratorv1.CancelPaymentRequest) gsresponse.Responder[orchestratorv1.CancelPaymentResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil { + return gsresponse.InvalidArgument[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) + } + paymentRef := strings.TrimSpace(req.GetPaymentRef()) + if paymentRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("payment_ref is required")) + } + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + payment, err := store.GetByPaymentRef(ctx, paymentRef) + if err != nil { + if err == storage.ErrPaymentNotFound { + return gsresponse.NotFound[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Auto[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if payment.State != model.PaymentStateAccepted { + reason := merrors.InvalidArgument("payment cannot be cancelled in current state") + return gsresponse.FailedPrecondition[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, "payment_not_cancellable", reason) + } + payment.State = model.PaymentStateCancelled + payment.FailureCode = model.PaymentFailureCodePolicy + payment.FailureReason = strings.TrimSpace(req.GetReason()) + if err := store.Update(ctx, payment); err != nil { + return gsresponse.Auto[orchestratorv1.CancelPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Success(&orchestratorv1.CancelPaymentResponse{Payment: toProtoPayment(payment)}) +} + +func (s *Service) getPaymentHandler(ctx context.Context, req *orchestratorv1.GetPaymentRequest) gsresponse.Responder[orchestratorv1.GetPaymentResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil { + return gsresponse.InvalidArgument[orchestratorv1.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) + } + paymentRef := strings.TrimSpace(req.GetPaymentRef()) + if paymentRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("payment_ref is required")) + } + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + entity, err := store.GetByPaymentRef(ctx, paymentRef) + if err != nil { + if err == storage.ErrPaymentNotFound { + return gsresponse.NotFound[orchestratorv1.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Auto[orchestratorv1.GetPaymentResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Success(&orchestratorv1.GetPaymentResponse{Payment: toProtoPayment(entity)}) +} + +func (s *Service) listPaymentsHandler(ctx context.Context, req *orchestratorv1.ListPaymentsRequest) gsresponse.Responder[orchestratorv1.ListPaymentsResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.ListPaymentsResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil { + return gsresponse.InvalidArgument[orchestratorv1.ListPaymentsResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) + } + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.ListPaymentsResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + filter := filterFromProto(req) + result, err := store.List(ctx, filter) + if err != nil { + return gsresponse.Auto[orchestratorv1.ListPaymentsResponse](s.logger, mservice.PaymentOrchestrator, err) + } + resp := &orchestratorv1.ListPaymentsResponse{ + Page: &paginationv1.CursorPageResponse{ + NextCursor: result.NextCursor, + }, + } + resp.Payments = make([]*orchestratorv1.Payment, 0, len(result.Items)) + for _, item := range result.Items { + resp.Payments = append(resp.Payments, toProtoPayment(item)) + } + return gsresponse.Success(resp) +} + +func (s *Service) initiateConversionHandler(ctx context.Context, req *orchestratorv1.InitiateConversionRequest) gsresponse.Responder[orchestratorv1.InitiateConversionResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("nil request")) + } + meta := req.GetMeta() + if meta == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("meta is required")) + } + orgRef := strings.TrimSpace(meta.GetOrganizationRef()) + if orgRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref is required")) + } + orgObjectID, parseErr := primitive.ObjectIDFromHex(orgRef) + if parseErr != nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("organization_ref must be a valid objectID")) + } + idempotencyKey := strings.TrimSpace(req.GetIdempotencyKey()) + if idempotencyKey == "" { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("idempotency_key is required")) + } + if req.GetSource() == nil || req.GetSource().GetLedger() == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("source ledger endpoint is required")) + } + if req.GetDestination() == nil || req.GetDestination().GetLedger() == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("destination ledger endpoint is required")) + } + fxIntent := req.GetFx() + if fxIntent == nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("fx intent is required")) + } + + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + + if existing, err := store.GetByIdempotencyKey(ctx, orgObjectID, idempotencyKey); err == nil && existing != nil { + return gsresponse.Success(&orchestratorv1.InitiateConversionResponse{Conversion: toProtoPayment(existing)}) + } else if err != nil && err != storage.ErrPaymentNotFound { + return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + amount, err := conversionAmountFromMetadata(req.GetMetadata(), fxIntent) + if err != nil { + return gsresponse.InvalidArgument[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + intentProto := &orchestratorv1.PaymentIntent{ + Kind: orchestratorv1.PaymentKind_PAYMENT_KIND_FX_CONVERSION, + Source: req.GetSource(), + Destination: req.GetDestination(), + Amount: amount, + RequiresFx: true, + Fx: fxIntent, + FeePolicy: req.GetFeePolicy(), + } + + quote, err := s.buildPaymentQuote(ctx, orgRef, &orchestratorv1.QuotePaymentRequest{ + Meta: req.GetMeta(), + IdempotencyKey: req.GetIdempotencyKey(), + Intent: intentProto, + }) + if err != nil { + return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + entity := &model.Payment{} + entity.SetID(primitive.NewObjectID()) + entity.SetOrganizationRef(orgObjectID) + entity.PaymentRef = entity.GetID().Hex() + entity.IdempotencyKey = idempotencyKey + entity.State = model.PaymentStateAccepted + entity.Intent = intentFromProto(intentProto) + entity.Metadata = cloneMetadata(req.GetMetadata()) + entity.LastQuote = quoteSnapshotToModel(quote) + entity.Normalize() + + if err = store.Create(ctx, entity); err != nil { + if err == storage.ErrDuplicatePayment { + return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, merrors.DataConflict("payment already exists")) + } + return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + if err := s.executePayment(ctx, store, entity, quote); err != nil { + return gsresponse.Auto[orchestratorv1.InitiateConversionResponse](s.logger, mservice.PaymentOrchestrator, err) + } + + return gsresponse.Success(&orchestratorv1.InitiateConversionResponse{ + Conversion: toProtoPayment(entity), + }) +} + +func (s *Service) processTransferUpdateHandler(ctx context.Context, req *orchestratorv1.ProcessTransferUpdateRequest) gsresponse.Responder[orchestratorv1.ProcessTransferUpdateResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil || req.GetEvent() == nil || req.GetEvent().GetTransfer() == nil { + return gsresponse.InvalidArgument[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("transfer event is required")) + } + transfer := req.GetEvent().GetTransfer() + transferRef := strings.TrimSpace(transfer.GetTransferRef()) + if transferRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("transfer_ref is required")) + } + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + payment, err := store.GetByChainTransferRef(ctx, transferRef) + if err != nil { + if err == storage.ErrPaymentNotFound { + return gsresponse.NotFound[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, err) + } + applyTransferStatus(req.GetEvent(), payment) + if err := store.Update(ctx, payment); err != nil { + return gsresponse.Auto[orchestratorv1.ProcessTransferUpdateResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Success(&orchestratorv1.ProcessTransferUpdateResponse{Payment: toProtoPayment(payment)}) +} + +func (s *Service) processDepositObservedHandler(ctx context.Context, req *orchestratorv1.ProcessDepositObservedRequest) gsresponse.Responder[orchestratorv1.ProcessDepositObservedResponse] { + if err := s.ensureRepository(ctx); err != nil { + return gsresponse.Unavailable[orchestratorv1.ProcessDepositObservedResponse](s.logger, mservice.PaymentOrchestrator, err) + } + if req == nil || req.GetEvent() == nil { + return gsresponse.InvalidArgument[orchestratorv1.ProcessDepositObservedResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("deposit event is required")) + } + event := req.GetEvent() + walletRef := strings.TrimSpace(event.GetWalletRef()) + if walletRef == "" { + return gsresponse.InvalidArgument[orchestratorv1.ProcessDepositObservedResponse](s.logger, mservice.PaymentOrchestrator, merrors.InvalidArgument("wallet_ref is required")) + } + store := s.storage.Payments() + if store == nil { + return gsresponse.Unavailable[orchestratorv1.ProcessDepositObservedResponse](s.logger, mservice.PaymentOrchestrator, errStorageUnavailable) + } + filter := &model.PaymentFilter{ + States: []model.PaymentState{model.PaymentStateSubmitted, model.PaymentStateFundsReserved}, + DestinationRef: walletRef, + } + result, err := store.List(ctx, filter) + if err != nil { + return gsresponse.Auto[orchestratorv1.ProcessDepositObservedResponse](s.logger, mservice.PaymentOrchestrator, err) + } + for _, payment := range result.Items { + if payment.Intent.Destination.Type != model.EndpointTypeManagedWallet { + continue + } + if !moneyEquals(payment.Intent.Amount, event.GetAmount()) { + continue + } + payment.State = model.PaymentStateSettled + payment.FailureCode = model.PaymentFailureCodeUnspecified + payment.FailureReason = "" + if payment.Execution == nil { + payment.Execution = &model.ExecutionRefs{} + } + if payment.Execution.ChainTransferRef == "" { + payment.Execution.ChainTransferRef = strings.TrimSpace(event.GetTransactionHash()) + } + if err := store.Update(ctx, payment); err != nil { + return gsresponse.Auto[orchestratorv1.ProcessDepositObservedResponse](s.logger, mservice.PaymentOrchestrator, err) + } + return gsresponse.Success(&orchestratorv1.ProcessDepositObservedResponse{Payment: toProtoPayment(payment)}) + } + return gsresponse.Success(&orchestratorv1.ProcessDepositObservedResponse{}) +} diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_test.go new file mode 100644 index 0000000..7a61ada --- /dev/null +++ b/api/payments/orchestrator/internal/service/orchestrator/service_test.go @@ -0,0 +1,290 @@ +package orchestrator + +import ( + "context" + "errors" + "strings" + "testing" + "time" + + chainclient "github.com/tech/sendico/chain/gateway/client" + ledgerclient "github.com/tech/sendico/ledger/client" + "github.com/tech/sendico/payments/orchestrator/storage" + "github.com/tech/sendico/payments/orchestrator/storage/model" + "github.com/tech/sendico/pkg/api/routers/gsresponse" + mo "github.com/tech/sendico/pkg/model" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestrator/v1" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func TestExecutePayment_FXConversionSettled(t *testing.T) { + ctx := context.Background() + + store := newStubPaymentsStore() + repo := &stubRepository{store: store} + svc := &Service{ + logger: zap.NewNop(), + clock: testClock{now: time.Now()}, + storage: repo, + ledger: ledgerDependency{client: &ledgerclient.Fake{ + ApplyFXWithChargesFn: func(ctx context.Context, req *ledgerv1.FXRequest) (*ledgerv1.PostResponse, error) { + return &ledgerv1.PostResponse{JournalEntryRef: "fx-entry"}, nil + }, + }}, + } + + payment := &model.Payment{ + PaymentRef: "fx-1", + IdempotencyKey: "fx-1", + OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: primitive.NewObjectID()}, + Intent: model.PaymentIntent{ + Kind: model.PaymentKindFXConversion, + Source: model.PaymentEndpoint{ + Type: model.EndpointTypeLedger, + Ledger: &model.LedgerEndpoint{LedgerAccountRef: "ledger:source"}, + }, + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeLedger, + Ledger: &model.LedgerEndpoint{LedgerAccountRef: "ledger:dest"}, + }, + Amount: &moneyv1.Money{Currency: "USD", Amount: "100"}, + }, + } + store.payments[payment.PaymentRef] = payment + + quote := &orchestratorv1.PaymentQuote{ + FxQuote: &oraclev1.Quote{ + QuoteRef: "quote-1", + BaseAmount: &moneyv1.Money{Currency: "USD", Amount: "100"}, + QuoteAmount: &moneyv1.Money{Currency: "EUR", Amount: "90"}, + Price: &moneyv1.Decimal{Value: "0.9"}, + }, + } + + if err := svc.executePayment(ctx, store, payment, quote); err != nil { + t.Fatalf("executePayment returned error: %v", err) + } + + if payment.State != model.PaymentStateSettled { + t.Fatalf("expected payment settled, got %s", payment.State) + } + if payment.Execution == nil || payment.Execution.FXEntryRef == "" { + t.Fatal("expected FX entry ref set on payment execution") + } +} + +func TestExecutePayment_ChainFailure(t *testing.T) { + ctx := context.Background() + + store := newStubPaymentsStore() + repo := &stubRepository{store: store} + svc := &Service{ + logger: zap.NewNop(), + clock: testClock{now: time.Now()}, + storage: repo, + gateway: gatewayDependency{client: &chainclient.Fake{ + SubmitTransferFn: func(ctx context.Context, req *gatewayv1.SubmitTransferRequest) (*gatewayv1.SubmitTransferResponse, error) { + return nil, errors.New("chain failure") + }, + }}, + } + + payment := &model.Payment{ + PaymentRef: "chain-1", + IdempotencyKey: "chain-1", + OrganizationBoundBase: mo.OrganizationBoundBase{OrganizationRef: primitive.NewObjectID()}, + Intent: model.PaymentIntent{ + Kind: model.PaymentKindPayout, + Source: model.PaymentEndpoint{ + Type: model.EndpointTypeManagedWallet, + ManagedWallet: &model.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-src", + }, + }, + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeManagedWallet, + ManagedWallet: &model.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-dst", + }, + }, + Amount: &moneyv1.Money{Currency: "USD", Amount: "50"}, + }, + } + store.payments[payment.PaymentRef] = payment + + err := svc.executePayment(ctx, store, payment, &orchestratorv1.PaymentQuote{}) + if err == nil || err.Error() != "chain failure" { + t.Fatalf("expected chain failure error, got %v", err) + } + if payment.State != model.PaymentStateFailed { + t.Fatalf("expected payment failed, got %s", payment.State) + } + if payment.FailureCode != model.PaymentFailureCodeChain { + t.Fatalf("expected failure code chain, got %s", payment.FailureCode) + } +} + +func TestProcessTransferUpdateHandler_Settled(t *testing.T) { + ctx := context.Background() + payment := &model.Payment{ + PaymentRef: "pay-1", + State: model.PaymentStateSubmitted, + Execution: &model.ExecutionRefs{ChainTransferRef: "transfer-1"}, + } + store := newStubPaymentsStore() + store.payments[payment.PaymentRef] = payment + store.byChain["transfer-1"] = payment + + svc := &Service{ + logger: zap.NewNop(), + clock: testClock{now: time.Now()}, + storage: &stubRepository{store: store}, + } + + req := &orchestratorv1.ProcessTransferUpdateRequest{ + Event: &gatewayv1.TransferStatusChangedEvent{ + Transfer: &gatewayv1.Transfer{ + TransferRef: "transfer-1", + Status: gatewayv1.TransferStatus_TRANSFER_CONFIRMED, + }, + }, + } + + reSP, err := gsresponse.Execute(ctx, svc.processTransferUpdateHandler(ctx, req)) + if err != nil { + t.Fatalf("handler returned error: %v", err) + } + if reSP.GetPayment().GetState() != orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED { + t.Fatalf("expected settled state, got %s", reSP.GetPayment().GetState()) + } +} + +func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) { + ctx := context.Background() + payment := &model.Payment{ + PaymentRef: "pay-2", + State: model.PaymentStateSubmitted, + Intent: model.PaymentIntent{ + Destination: model.PaymentEndpoint{ + Type: model.EndpointTypeManagedWallet, + ManagedWallet: &model.ManagedWalletEndpoint{ + ManagedWalletRef: "wallet-dst", + }, + }, + Amount: &moneyv1.Money{Currency: "USD", Amount: "40"}, + }, + } + store := newStubPaymentsStore() + store.listResp = &model.PaymentList{Items: []*model.Payment{payment}} + store.payments[payment.PaymentRef] = payment + + svc := &Service{ + logger: zap.NewNop(), + clock: testClock{now: time.Now()}, + storage: &stubRepository{store: store}, + } + + req := &orchestratorv1.ProcessDepositObservedRequest{ + Event: &gatewayv1.WalletDepositObservedEvent{ + WalletRef: "wallet-dst", + Amount: &moneyv1.Money{Currency: "USD", Amount: "40"}, + }, + } + + reSP, err := gsresponse.Execute(ctx, svc.processDepositObservedHandler(ctx, req)) + if err != nil { + t.Fatalf("handler returned error: %v", err) + } + if reSP.GetPayment().GetState() != orchestratorv1.PaymentState_PAYMENT_STATE_SETTLED { + t.Fatalf("expected settled state, got %s", reSP.GetPayment().GetState()) + } +} + +// ---------------------------------------------------------------------- + +type stubRepository struct { + store *stubPaymentsStore +} + +func (r *stubRepository) Ping(context.Context) error { return nil } +func (r *stubRepository) Payments() storage.PaymentsStore { return r.store } + +type stubPaymentsStore struct { + payments map[string]*model.Payment + byChain map[string]*model.Payment + listResp *model.PaymentList +} + +func newStubPaymentsStore() *stubPaymentsStore { + return &stubPaymentsStore{ + payments: map[string]*model.Payment{}, + byChain: map[string]*model.Payment{}, + } +} + +func (s *stubPaymentsStore) Create(ctx context.Context, payment *model.Payment) error { + if _, exists := s.payments[payment.PaymentRef]; exists { + return storage.ErrDuplicatePayment + } + s.payments[payment.PaymentRef] = payment + if payment.Execution != nil && payment.Execution.ChainTransferRef != "" { + s.byChain[payment.Execution.ChainTransferRef] = payment + } + return nil +} + +func (s *stubPaymentsStore) Update(ctx context.Context, payment *model.Payment) error { + if _, exists := s.payments[payment.PaymentRef]; !exists { + return storage.ErrPaymentNotFound + } + s.payments[payment.PaymentRef] = payment + if payment.Execution != nil && payment.Execution.ChainTransferRef != "" { + s.byChain[payment.Execution.ChainTransferRef] = payment + } + return nil +} + +func (s *stubPaymentsStore) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.Payment, error) { + if p, ok := s.payments[paymentRef]; ok { + return p, nil + } + return nil, storage.ErrPaymentNotFound +} + +func (s *stubPaymentsStore) GetByIdempotencyKey(ctx context.Context, orgRef primitive.ObjectID, key string) (*model.Payment, error) { + for _, p := range s.payments { + if p.OrganizationRef == orgRef && strings.TrimSpace(p.IdempotencyKey) == key { + return p, nil + } + } + return nil, storage.ErrPaymentNotFound +} + +func (s *stubPaymentsStore) GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error) { + if p, ok := s.byChain[transferRef]; ok { + return p, nil + } + return nil, storage.ErrPaymentNotFound +} + +func (s *stubPaymentsStore) List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error) { + if s.listResp != nil { + return s.listResp, nil + } + return &model.PaymentList{}, nil +} + +var _ storage.PaymentsStore = (*stubPaymentsStore)(nil) + +// testClock satisfies clock.Clock + +type testClock struct { + now time.Time +} + +func (c testClock) Now() time.Time { return c.now } diff --git a/api/payments/orchestrator/storage/model/payment.go b/api/payments/orchestrator/storage/model/payment.go new file mode 100644 index 0000000..7ca9205 --- /dev/null +++ b/api/payments/orchestrator/storage/model/payment.go @@ -0,0 +1,226 @@ +package model + +import ( + "strings" + + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" + gatewayv1 "github.com/tech/sendico/pkg/proto/chain/gateway/v1" + fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" + moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" +) + +// PaymentKind captures the orchestrator intent type. +type PaymentKind string + +const ( + PaymentKindUnspecified PaymentKind = "unspecified" + PaymentKindPayout PaymentKind = "payout" + PaymentKindInternalTransfer PaymentKind = "internal_transfer" + PaymentKindFXConversion PaymentKind = "fx_conversion" +) + +// PaymentState enumerates lifecycle phases. +type PaymentState string + +const ( + PaymentStateUnspecified PaymentState = "unspecified" + PaymentStateAccepted PaymentState = "accepted" + PaymentStateFundsReserved PaymentState = "funds_reserved" + PaymentStateSubmitted PaymentState = "submitted" + PaymentStateSettled PaymentState = "settled" + PaymentStateFailed PaymentState = "failed" + PaymentStateCancelled PaymentState = "cancelled" +) + +// PaymentFailureCode captures terminal reasons. +type PaymentFailureCode string + +const ( + PaymentFailureCodeUnspecified PaymentFailureCode = "unspecified" + PaymentFailureCodeBalance PaymentFailureCode = "balance" + PaymentFailureCodeLedger PaymentFailureCode = "ledger" + PaymentFailureCodeFX PaymentFailureCode = "fx" + PaymentFailureCodeChain PaymentFailureCode = "chain" + PaymentFailureCodeFees PaymentFailureCode = "fees" + PaymentFailureCodePolicy PaymentFailureCode = "policy" +) + +// PaymentEndpointType indicates how value should be routed. +type PaymentEndpointType string + +const ( + EndpointTypeUnspecified PaymentEndpointType = "unspecified" + EndpointTypeLedger PaymentEndpointType = "ledger" + EndpointTypeManagedWallet PaymentEndpointType = "managed_wallet" + EndpointTypeExternalChain PaymentEndpointType = "external_chain" +) + +// LedgerEndpoint describes ledger routing. +type LedgerEndpoint struct { + LedgerAccountRef string `bson:"ledgerAccountRef" json:"ledgerAccountRef"` + ContraLedgerAccountRef string `bson:"contraLedgerAccountRef,omitempty" json:"contraLedgerAccountRef,omitempty"` +} + +// ManagedWalletEndpoint describes managed wallet routing. +type ManagedWalletEndpoint struct { + ManagedWalletRef string `bson:"managedWalletRef" json:"managedWalletRef"` + Asset *gatewayv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"` +} + +// ExternalChainEndpoint describes an external address. +type ExternalChainEndpoint struct { + Asset *gatewayv1.Asset `bson:"asset,omitempty" json:"asset,omitempty"` + Address string `bson:"address" json:"address"` + Memo string `bson:"memo,omitempty" json:"memo,omitempty"` +} + +// PaymentEndpoint is a polymorphic payment destination/source. +type PaymentEndpoint struct { + Type PaymentEndpointType `bson:"type" json:"type"` + Ledger *LedgerEndpoint `bson:"ledger,omitempty" json:"ledger,omitempty"` + ManagedWallet *ManagedWalletEndpoint `bson:"managedWallet,omitempty" json:"managedWallet,omitempty"` + ExternalChain *ExternalChainEndpoint `bson:"externalChain,omitempty" json:"externalChain,omitempty"` + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// FXIntent captures FX conversion preferences. +type FXIntent struct { + Pair *fxv1.CurrencyPair `bson:"pair,omitempty" json:"pair,omitempty"` + Side fxv1.Side `bson:"side,omitempty" json:"side,omitempty"` + Firm bool `bson:"firm,omitempty" json:"firm,omitempty"` + TTLMillis int64 `bson:"ttlMillis,omitempty" json:"ttlMillis,omitempty"` + PreferredProvider string `bson:"preferredProvider,omitempty" json:"preferredProvider,omitempty"` + MaxAgeMillis int32 `bson:"maxAgeMillis,omitempty" json:"maxAgeMillis,omitempty"` +} + +// PaymentIntent models the requested payment operation. +type PaymentIntent struct { + Kind PaymentKind `bson:"kind" json:"kind"` + Source PaymentEndpoint `bson:"source" json:"source"` + Destination PaymentEndpoint `bson:"destination" json:"destination"` + Amount *moneyv1.Money `bson:"amount" json:"amount"` + RequiresFX bool `bson:"requiresFx,omitempty" json:"requiresFx,omitempty"` + FX *FXIntent `bson:"fx,omitempty" json:"fx,omitempty"` + FeePolicy *feesv1.PolicyOverrides `bson:"feePolicy,omitempty" json:"feePolicy,omitempty"` + Attributes map[string]string `bson:"attributes,omitempty" json:"attributes,omitempty"` +} + +// PaymentQuoteSnapshot stores the latest quote info. +type PaymentQuoteSnapshot struct { + DebitAmount *moneyv1.Money `bson:"debitAmount,omitempty" json:"debitAmount,omitempty"` + ExpectedSettlementAmount *moneyv1.Money `bson:"expectedSettlementAmount,omitempty" json:"expectedSettlementAmount,omitempty"` + ExpectedFeeTotal *moneyv1.Money `bson:"expectedFeeTotal,omitempty" json:"expectedFeeTotal,omitempty"` + FeeLines []*feesv1.DerivedPostingLine `bson:"feeLines,omitempty" json:"feeLines,omitempty"` + FeeRules []*feesv1.AppliedRule `bson:"feeRules,omitempty" json:"feeRules,omitempty"` + FXQuote *oraclev1.Quote `bson:"fxQuote,omitempty" json:"fxQuote,omitempty"` + NetworkFee *gatewayv1.EstimateTransferFeeResponse `bson:"networkFee,omitempty" json:"networkFee,omitempty"` + FeeQuoteToken string `bson:"feeQuoteToken,omitempty" json:"feeQuoteToken,omitempty"` +} + +// ExecutionRefs links to downstream systems. +type ExecutionRefs struct { + DebitEntryRef string `bson:"debitEntryRef,omitempty" json:"debitEntryRef,omitempty"` + CreditEntryRef string `bson:"creditEntryRef,omitempty" json:"creditEntryRef,omitempty"` + FXEntryRef string `bson:"fxEntryRef,omitempty" json:"fxEntryRef,omitempty"` + ChainTransferRef string `bson:"chainTransferRef,omitempty" json:"chainTransferRef,omitempty"` +} + +// Payment persists orchestrated payment lifecycle. +type Payment struct { + storable.Base `bson:",inline" json:",inline"` + model.OrganizationBoundBase `bson:",inline" json:",inline"` + + PaymentRef string `bson:"paymentRef" json:"paymentRef"` + IdempotencyKey string `bson:"idempotencyKey" json:"idempotencyKey"` + Intent PaymentIntent `bson:"intent" json:"intent"` + State PaymentState `bson:"state" json:"state"` + FailureCode PaymentFailureCode `bson:"failureCode,omitempty" json:"failureCode,omitempty"` + FailureReason string `bson:"failureReason,omitempty" json:"failureReason,omitempty"` + LastQuote *PaymentQuoteSnapshot `bson:"lastQuote,omitempty" json:"lastQuote,omitempty"` + Execution *ExecutionRefs `bson:"execution,omitempty" json:"execution,omitempty"` + Metadata map[string]string `bson:"metadata,omitempty" json:"metadata,omitempty"` +} + +// Collection implements storable.Storable. +func (*Payment) Collection() string { + return mservice.Payments +} + +// PaymentFilter enables filtered queries. +type PaymentFilter struct { + States []PaymentState + SourceRef string + DestinationRef string + Cursor string + Limit int32 +} + +// PaymentList contains paginated results. +type PaymentList struct { + Items []*Payment + NextCursor string +} + +// Normalize harmonises string fields for indexing and comparisons. +func (p *Payment) Normalize() { + p.PaymentRef = strings.TrimSpace(p.PaymentRef) + p.IdempotencyKey = strings.TrimSpace(p.IdempotencyKey) + p.FailureReason = strings.TrimSpace(p.FailureReason) + if p.Metadata != nil { + for k, v := range p.Metadata { + p.Metadata[k] = strings.TrimSpace(v) + } + } + normalizeEndpoint(&p.Intent.Source) + normalizeEndpoint(&p.Intent.Destination) + if p.Intent.Attributes != nil { + for k, v := range p.Intent.Attributes { + p.Intent.Attributes[k] = strings.TrimSpace(v) + } + } + if p.Execution != nil { + p.Execution.DebitEntryRef = strings.TrimSpace(p.Execution.DebitEntryRef) + p.Execution.CreditEntryRef = strings.TrimSpace(p.Execution.CreditEntryRef) + p.Execution.FXEntryRef = strings.TrimSpace(p.Execution.FXEntryRef) + p.Execution.ChainTransferRef = strings.TrimSpace(p.Execution.ChainTransferRef) + } +} + +func normalizeEndpoint(ep *PaymentEndpoint) { + if ep == nil { + return + } + if ep.Metadata != nil { + for k, v := range ep.Metadata { + ep.Metadata[k] = strings.TrimSpace(v) + } + } + switch ep.Type { + case EndpointTypeLedger: + if ep.Ledger != nil { + ep.Ledger.LedgerAccountRef = strings.TrimSpace(ep.Ledger.LedgerAccountRef) + ep.Ledger.ContraLedgerAccountRef = strings.TrimSpace(ep.Ledger.ContraLedgerAccountRef) + } + case EndpointTypeManagedWallet: + if ep.ManagedWallet != nil { + ep.ManagedWallet.ManagedWalletRef = strings.TrimSpace(ep.ManagedWallet.ManagedWalletRef) + if ep.ManagedWallet.Asset != nil { + ep.ManagedWallet.Asset.TokenSymbol = strings.TrimSpace(strings.ToUpper(ep.ManagedWallet.Asset.TokenSymbol)) + ep.ManagedWallet.Asset.ContractAddress = strings.TrimSpace(strings.ToLower(ep.ManagedWallet.Asset.ContractAddress)) + } + } + case EndpointTypeExternalChain: + if ep.ExternalChain != nil { + ep.ExternalChain.Address = strings.TrimSpace(strings.ToLower(ep.ExternalChain.Address)) + ep.ExternalChain.Memo = strings.TrimSpace(ep.ExternalChain.Memo) + if ep.ExternalChain.Asset != nil { + ep.ExternalChain.Asset.TokenSymbol = strings.TrimSpace(strings.ToUpper(ep.ExternalChain.Asset.TokenSymbol)) + ep.ExternalChain.Asset.ContractAddress = strings.TrimSpace(strings.ToLower(ep.ExternalChain.Asset.ContractAddress)) + } + } + } +} diff --git a/api/payments/orchestrator/storage/mongo/repository.go b/api/payments/orchestrator/storage/mongo/repository.go new file mode 100644 index 0000000..6074102 --- /dev/null +++ b/api/payments/orchestrator/storage/mongo/repository.go @@ -0,0 +1,68 @@ +package mongo + +import ( + "context" + + "github.com/tech/sendico/payments/orchestrator/storage" + "github.com/tech/sendico/payments/orchestrator/storage/model" + "github.com/tech/sendico/payments/orchestrator/storage/mongo/store" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" +) + +// Store implements storage.Repository backed by MongoDB. +type Store struct { + logger mlogger.Logger + ping func(context.Context) error + + payments storage.PaymentsStore +} + +// New constructs a Mongo-backed payments repository from a Mongo connection. +func New(logger mlogger.Logger, conn *db.MongoConnection) (*Store, error) { + if conn == nil { + return nil, merrors.InvalidArgument("payments.storage.mongo: connection is nil") + } + repo := repository.CreateMongoRepository(conn.Database(), (&model.Payment{}).Collection()) + return NewWithRepository(logger, conn.Ping, repo) +} + +// NewWithRepository constructs a payments repository using the provided primitives. +func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository) (*Store, error) { + if ping == nil { + return nil, merrors.InvalidArgument("payments.storage.mongo: ping func is nil") + } + if paymentsRepo == nil { + return nil, merrors.InvalidArgument("payments.storage.mongo: payments repository is nil") + } + + childLogger := logger.Named("storage").Named("mongo") + paymentsStore, err := store.NewPayments(childLogger, paymentsRepo) + if err != nil { + return nil, err + } + result := &Store{ + logger: childLogger, + ping: ping, + payments: paymentsStore, + } + + return result, nil +} + +// Ping verifies connectivity with the backing database. +func (s *Store) Ping(ctx context.Context) error { + if s.ping == nil { + return merrors.InvalidArgument("payments.storage.mongo: ping func is nil") + } + return s.ping(ctx) +} + +// Payments returns the payments store. +func (s *Store) Payments() storage.PaymentsStore { + return s.payments +} + +var _ storage.Repository = (*Store)(nil) diff --git a/api/payments/orchestrator/storage/mongo/store/payments.go b/api/payments/orchestrator/storage/mongo/store/payments.go new file mode 100644 index 0000000..4e2dd18 --- /dev/null +++ b/api/payments/orchestrator/storage/mongo/store/payments.go @@ -0,0 +1,266 @@ +package store + +import ( + "context" + "errors" + "strings" + + "github.com/tech/sendico/payments/orchestrator/storage" + "github.com/tech/sendico/payments/orchestrator/storage/model" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +const ( + defaultPaymentPageSize int64 = 50 + maxPaymentPageSize int64 = 200 +) + +type Payments struct { + logger mlogger.Logger + repo repository.Repository +} + +// NewPayments constructs a Mongo-backed payments store. +func NewPayments(logger mlogger.Logger, repo repository.Repository) (*Payments, error) { + if repo == nil { + return nil, merrors.InvalidArgument("paymentsStore: repository is nil") + } + + indexes := []*ri.Definition{ + { + Keys: []ri.Key{{Field: "paymentRef", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "idempotencyKey", Sort: ri.Asc}, {Field: "organizationRef", Sort: ri.Asc}}, + Unique: true, + }, + { + Keys: []ri.Key{{Field: "state", Sort: ri.Asc}}, + }, + { + Keys: []ri.Key{{Field: "intent.source.managedWallet.managedWalletRef", Sort: ri.Asc}}, + }, + { + Keys: []ri.Key{{Field: "intent.destination.managedWallet.managedWalletRef", Sort: ri.Asc}}, + }, + { + Keys: []ri.Key{{Field: "execution.chainTransferRef", Sort: ri.Asc}}, + }, + } + + for _, def := range indexes { + if err := repo.CreateIndex(def); err != nil { + logger.Error("failed to ensure payments index", zap.Error(err), zap.String("collection", repo.Collection())) + return nil, err + } + } + + childLogger := logger.Named("payments") + childLogger.Debug("payments store initialised") + + return &Payments{ + logger: childLogger, + repo: repo, + }, nil +} + +func (p *Payments) Create(ctx context.Context, payment *model.Payment) error { + if payment == nil { + return merrors.InvalidArgument("paymentsStore: nil payment") + } + payment.Normalize() + if payment.PaymentRef == "" { + return merrors.InvalidArgument("paymentsStore: empty paymentRef") + } + if strings.TrimSpace(payment.IdempotencyKey) == "" { + return merrors.InvalidArgument("paymentsStore: empty idempotencyKey") + } + if payment.OrganizationRef == primitive.NilObjectID { + return merrors.InvalidArgument("paymentsStore: organization_ref is required") + } + + payment.Update() + filter := repository.OrgFilter(payment.OrganizationRef).And( + repository.Filter("idempotencyKey", payment.IdempotencyKey), + ) + + if err := p.repo.Insert(ctx, payment, filter); err != nil { + if errors.Is(err, merrors.ErrDataConflict) { + return storage.ErrDuplicatePayment + } + return err + } + p.logger.Debug("payment created", zap.String("payment_ref", payment.PaymentRef)) + return nil +} + +func (p *Payments) Update(ctx context.Context, payment *model.Payment) error { + if payment == nil { + return merrors.InvalidArgument("paymentsStore: nil payment") + } + if payment.ID.IsZero() { + return merrors.InvalidArgument("paymentsStore: missing payment id") + } + payment.Normalize() + payment.Update() + if err := p.repo.Update(ctx, payment); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return storage.ErrPaymentNotFound + } + return err + } + return nil +} + +func (p *Payments) GetByPaymentRef(ctx context.Context, paymentRef string) (*model.Payment, error) { + paymentRef = strings.TrimSpace(paymentRef) + if paymentRef == "" { + return nil, merrors.InvalidArgument("paymentsStore: empty paymentRef") + } + entity := &model.Payment{} + if err := p.repo.FindOneByFilter(ctx, repository.Filter("paymentRef", paymentRef), entity); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return nil, storage.ErrPaymentNotFound + } + return nil, err + } + return entity, nil +} + +func (p *Payments) GetByIdempotencyKey(ctx context.Context, orgRef primitive.ObjectID, idempotencyKey string) (*model.Payment, error) { + idempotencyKey = strings.TrimSpace(idempotencyKey) + if orgRef == primitive.NilObjectID { + return nil, merrors.InvalidArgument("paymentsStore: organization_ref is required") + } + if idempotencyKey == "" { + return nil, merrors.InvalidArgument("paymentsStore: empty idempotencyKey") + } + entity := &model.Payment{} + query := repository.OrgFilter(orgRef).And(repository.Filter("idempotencyKey", idempotencyKey)) + if err := p.repo.FindOneByFilter(ctx, query, entity); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return nil, storage.ErrPaymentNotFound + } + return nil, err + } + return entity, nil +} + +func (p *Payments) GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error) { + transferRef = strings.TrimSpace(transferRef) + if transferRef == "" { + return nil, merrors.InvalidArgument("paymentsStore: empty chain transfer reference") + } + entity := &model.Payment{} + if err := p.repo.FindOneByFilter(ctx, repository.Filter("execution.chainTransferRef", transferRef), entity); err != nil { + if errors.Is(err, merrors.ErrNoData) { + return nil, storage.ErrPaymentNotFound + } + return nil, err + } + return entity, nil +} + +func (p *Payments) List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error) { + if filter == nil { + filter = &model.PaymentFilter{} + } + + query := repository.Query() + + if len(filter.States) > 0 { + states := make([]string, 0, len(filter.States)) + for _, state := range filter.States { + if trimmed := strings.TrimSpace(string(state)); trimmed != "" { + states = append(states, trimmed) + } + } + if len(states) > 0 { + query = query.Comparison(repository.Field("state"), builder.In, states) + } + } + + if ref := strings.TrimSpace(filter.SourceRef); ref != "" { + if endpointFilter := endpointQuery("intent.source", ref); endpointFilter != nil { + query = query.And(endpointFilter) + } + } + + if ref := strings.TrimSpace(filter.DestinationRef); ref != "" { + if endpointFilter := endpointQuery("intent.destination", ref); endpointFilter != nil { + query = query.And(endpointFilter) + } + } + + if cursor := strings.TrimSpace(filter.Cursor); cursor != "" { + if oid, err := primitive.ObjectIDFromHex(cursor); err == nil { + query = query.Comparison(repository.IDField(), builder.Gt, oid) + } else { + p.logger.Warn("ignoring invalid payments cursor", zap.String("cursor", cursor), zap.Error(err)) + } + } + + limit := sanitizePaymentLimit(filter.Limit) + fetchLimit := limit + 1 + query = query.Sort(repository.IDField(), true).Limit(&fetchLimit) + + payments := make([]*model.Payment, 0, fetchLimit) + decoder := func(cur *mongo.Cursor) error { + item := &model.Payment{} + if err := cur.Decode(item); err != nil { + return err + } + payments = append(payments, item) + return nil + } + + if err := p.repo.FindManyByFilter(ctx, query, decoder); err != nil && !errors.Is(err, merrors.ErrNoData) { + return nil, err + } + + nextCursor := "" + if int64(len(payments)) == fetchLimit { + last := payments[len(payments)-1] + nextCursor = last.ID.Hex() + payments = payments[:len(payments)-1] + } + + return &model.PaymentList{ + Items: payments, + NextCursor: nextCursor, + }, nil +} + +func endpointQuery(prefix, ref string) builder.Query { + trimmed := strings.TrimSpace(ref) + if trimmed == "" { + return nil + } + + lower := strings.ToLower(trimmed) + filters := []builder.Query{ + repository.Filter(prefix+".ledger.ledgerAccountRef", trimmed), + repository.Filter(prefix+".managedWallet.managedWalletRef", trimmed), + repository.Filter(prefix+".externalChain.address", lower), + } + + return repository.Query().Or(filters...) +} + +func sanitizePaymentLimit(requested int32) int64 { + if requested <= 0 { + return defaultPaymentPageSize + } + if requested > int32(maxPaymentPageSize) { + return maxPaymentPageSize + } + return int64(requested) +} diff --git a/api/payments/orchestrator/storage/storage.go b/api/payments/orchestrator/storage/storage.go new file mode 100644 index 0000000..df6bb38 --- /dev/null +++ b/api/payments/orchestrator/storage/storage.go @@ -0,0 +1,37 @@ +package storage + +import ( + "context" + + "github.com/tech/sendico/payments/orchestrator/storage/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type storageError string + +func (e storageError) Error() string { + return string(e) +} + +var ( + // ErrPaymentNotFound signals that a payment record does not exist. + ErrPaymentNotFound = storageError("payments.orchestrator.storage: payment not found") + // ErrDuplicatePayment signals that idempotency constraints were violated. + ErrDuplicatePayment = storageError("payments.orchestrator.storage: duplicate payment") +) + +// Repository exposes persistence primitives for the orchestrator domain. +type Repository interface { + Ping(ctx context.Context) error + Payments() PaymentsStore +} + +// PaymentsStore manages payment lifecycle state. +type PaymentsStore interface { + Create(ctx context.Context, payment *model.Payment) error + Update(ctx context.Context, payment *model.Payment) error + GetByPaymentRef(ctx context.Context, paymentRef string) (*model.Payment, error) + GetByIdempotencyKey(ctx context.Context, orgRef primitive.ObjectID, idempotencyKey string) (*model.Payment, error) + GetByChainTransferRef(ctx context.Context, transferRef string) (*model.Payment, error) + List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error) +} diff --git a/api/pkg/.DS_Store b/api/pkg/.DS_Store new file mode 100644 index 0000000000000000000000000000000000000000..1aa90dd38745402468a211e4eb99a4992b4db050 GIT binary patch literal 6148 zcmeH~K?=e^3`G;|qTr@Wm$UHz-e3?tK`)>n=t3%px}Kx^lL>;=wTS#c@+X-IrEk$` zL_}A&{Zgb8krr+$3kxGt4X|{^>yQ5dhkt?1r_^63}D?Xbvq^QGsbp4;roN zV~Ewg9h%}?4lPw{yJ!p_8c$Z6VqjX^MH3R3W)}t$Ab}BqY0W#k|2ObY^Z%%YDG89k zpApbz-LF@8skmF;UeD^=sM@-~p?)0U_8}^eepgFWuMFqwm0mr~V0$(NY E0L2Ru#Q*>R literal 0 HcmV?d00001 diff --git a/api/pkg/.gitignore b/api/pkg/.gitignore new file mode 100644 index 0000000..c8abcaa --- /dev/null +++ b/api/pkg/.gitignore @@ -0,0 +1,6 @@ +proto/billing +proto/common +proto/chain +proto/ledger +proto/oracle +proto/payments \ No newline at end of file diff --git a/api/pkg/api/http/methods.go b/api/pkg/api/http/methods.go new file mode 100644 index 0000000..ec73a29 --- /dev/null +++ b/api/pkg/api/http/methods.go @@ -0,0 +1,36 @@ +package api + +import "fmt" + +type HTTPMethod int + +const ( + Get HTTPMethod = iota + Post + Put + Patch + Delete + Options + Head +) + +func HTTPMethod2String(method HTTPMethod) string { + switch method { + case Get: + return "GET" + case Post: + return "POST" + case Put: + return "PUT" + case Delete: + return "DELETE" + case Patch: + return "PATCH" + case Options: + return "OPTIONS" + case Head: + return "HEAD" + default: + return fmt.Sprintf("unknown: %d", method) + } +} diff --git a/api/pkg/api/http/response/response.go b/api/pkg/api/http/response/response.go new file mode 100644 index 0000000..55fea99 --- /dev/null +++ b/api/pkg/api/http/response/response.go @@ -0,0 +1,205 @@ +package response + +import ( + "encoding/json" + "errors" + "fmt" + "net/http" + + api "github.com/tech/sendico/pkg/api/http" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "go.uber.org/zap" +) + +// BaseResponse is a general structure for all API responses. +type BaseResponse struct { + Status string `json:"status"` // "success" or "error" + Data any `json:"data"` // The actual data payload or the error details +} + +// ErrorResponse provides more details about an error. +type ErrorResponse struct { + Code int `json:"code"` // A unique identifier for the error type, useful for client handling + Error string `json:"error"` + Source string `json:"source"` + Details string `json:"details"` // Additional details or hints about the error, if necessary +} + +func errMessage(err error) string { + if err != nil { + return err.Error() + } + return "" +} + +func logRequest(logger mlogger.Logger, r *http.Request, message string) { + logger.Debug( + message, + zap.String("host", r.Host), + zap.String("address", r.RemoteAddr), + zap.String("method", r.Method), + zap.String("request_uri", r.RequestURI), + zap.String("proto", r.Proto), + zap.String("user_agent", r.UserAgent()), + ) +} + +func writeJSON(logger mlogger.Logger, w http.ResponseWriter, r *http.Request, code int, payload any) { + w.Header().Set("Content-Type", "application/json; charset=UTF-8") + w.WriteHeader(code) + if err := json.NewEncoder(w).Encode(&payload); err != nil { + logger.Warn("Failed to encode JSON response", + zap.Error(err), + zap.Any("response", payload), + zap.String("host", r.Host), + zap.String("address", r.RemoteAddr), + zap.String("method", r.Method), + zap.String("request_uri", r.RequestURI), + zap.String("proto", r.Proto), + zap.String("user_agent", r.UserAgent())) + } +} + +func errorf( + logger mlogger.Logger, + w http.ResponseWriter, r *http.Request, + source mservice.Type, code int, message, details string, +) { + logRequest(logger, r, message) + + errorMessage := BaseResponse{ + Status: api.MSError, + Data: ErrorResponse{ + Code: code, + Details: details, + Source: source, + Error: message, + }, + } + + writeJSON(logger, w, r, code, errorMessage) +} + +func Accepted(logger mlogger.Logger, data any) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp := BaseResponse{ + Status: api.MSProcessed, + Data: data, + } + writeJSON(logger, w, r, http.StatusAccepted, resp) + } +} + +func Ok(logger mlogger.Logger, data any) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp := BaseResponse{ + Status: api.MSSuccess, + Data: data, + } + writeJSON(logger, w, r, http.StatusOK, resp) + } +} + +func Created(logger mlogger.Logger, data any) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + resp := BaseResponse{ + Status: api.MSSuccess, + Data: data, + } + writeJSON(logger, w, r, http.StatusCreated, resp) + } +} + +func Auto(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc { + if err == nil { + return Success(logger) + } + if errors.Is(err, merrors.ErrAccessDenied) { + return AccessDenied(logger, source, errMessage(err)) + } + if errors.Is(err, merrors.ErrDataConflict) { + return DataConflict(logger, source, errMessage(err)) + } + if errors.Is(err, merrors.ErrInvalidArg) { + return BadRequest(logger, source, "invalid_argument", errMessage(err)) + } + if errors.Is(err, merrors.ErrNoData) { + return NotFound(logger, source, errMessage(err)) + } + if errors.Is(err, merrors.ErrUnauthorized) { + return Unauthorized(logger, source, errMessage(err)) + } + return Internal(logger, source, err) +} + +func Internal(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusInternalServerError, "internal_error", errMessage(err)) + } +} + +func NotImplemented(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusNotImplemented, "not_implemented", hint) + } +} + +func BadRequest(logger mlogger.Logger, source mservice.Type, err, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusBadRequest, err, hint) + } +} + +func BadQueryParam(logger mlogger.Logger, source mservice.Type, param string, err error) http.HandlerFunc { + return BadRequest(logger, source, "invalid_query_parameter", fmt.Sprintf("Failed to parse '%s': %v", param, err)) +} + +func BadReference(logger mlogger.Logger, source mservice.Type, refName, refVal string, err error) http.HandlerFunc { + return BadRequest(logger, source, "broken_reference", + fmt.Sprintf("broken object reference: %s = %s, error: %v", refName, refVal, err)) +} + +func BadPayload(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc { + return BadRequest(logger, source, "broken_payload", + fmt.Sprintf("broken '%s' object payload, error: %v", source, err)) +} + +func DataConflict(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusConflict, "data_conflict", hint) + } +} + +func Error(logger mlogger.Logger, source mservice.Type, code int, errType, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, code, errType, hint) + } +} + +func AccessDenied(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc { + return Error(logger, source, http.StatusForbidden, "access_denied", hint) +} + +func Forbidden(logger mlogger.Logger, source mservice.Type, errType, hint string) http.HandlerFunc { + return Error(logger, source, http.StatusForbidden, errType, hint) +} + +func LicenseRequired(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusPaymentRequired, "license_required", hint) + } +} + +func Unauthorized(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusUnauthorized, "unauthorized", hint) + } +} + +func NotFound(logger mlogger.Logger, source mservice.Type, hint string) http.HandlerFunc { + return func(w http.ResponseWriter, r *http.Request) { + errorf(logger, w, r, source, http.StatusNotFound, "not_found", hint) + } +} diff --git a/api/pkg/api/http/response/result.go b/api/pkg/api/http/response/result.go new file mode 100644 index 0000000..aa65a48 --- /dev/null +++ b/api/pkg/api/http/response/result.go @@ -0,0 +1,19 @@ +package response + +import ( + "net/http" + + "github.com/tech/sendico/pkg/mlogger" +) + +type Result struct { + Result bool `json:"result"` +} + +func Success(logger mlogger.Logger) http.HandlerFunc { + return Ok(logger, Result{Result: true}) +} + +func Failed(logger mlogger.Logger) http.HandlerFunc { + return Accepted(logger, Result{Result: false}) +} diff --git a/api/pkg/api/http/status.go b/api/pkg/api/http/status.go new file mode 100644 index 0000000..76f0b7a --- /dev/null +++ b/api/pkg/api/http/status.go @@ -0,0 +1,8 @@ +package api + +const ( + MSSuccess string = "success" + MSProcessed string = "processed" + MSError string = "error" + MSRequest string = "request" +) diff --git a/api/pkg/api/routers/grpc.go b/api/pkg/api/routers/grpc.go new file mode 100644 index 0000000..eb83604 --- /dev/null +++ b/api/pkg/api/routers/grpc.go @@ -0,0 +1,61 @@ +package routers + +import ( + "context" + "net" + + "github.com/tech/sendico/pkg/api/routers/internal/grpcimp" + "github.com/tech/sendico/pkg/mlogger" + "google.golang.org/grpc" +) + +type ( + GRPCServiceRegistration = func(grpc.ServiceRegistrar) +) + +type GRPC interface { + Register(registration GRPCServiceRegistration) error + Start(ctx context.Context) error + Finish(ctx context.Context) error + Addr() net.Addr + Done() <-chan error +} + +type ( + GRPCConfig = grpcimp.Config + GRPCTLSConfig = grpcimp.TLSConfig +) + +type GRPCOption func(*grpcimp.Options) + +func WithUnaryInterceptors(interceptors ...grpc.UnaryServerInterceptor) GRPCOption { + return func(o *grpcimp.Options) { + o.UnaryInterceptors = append(o.UnaryInterceptors, interceptors...) + } +} + +func WithStreamInterceptors(interceptors ...grpc.StreamServerInterceptor) GRPCOption { + return func(o *grpcimp.Options) { + o.StreamInterceptors = append(o.StreamInterceptors, interceptors...) + } +} + +func WithListener(listener net.Listener) GRPCOption { + return func(o *grpcimp.Options) { + o.Listener = listener + } +} + +func WithServerOptions(opts ...grpc.ServerOption) GRPCOption { + return func(o *grpcimp.Options) { + o.ServerOptions = append(o.ServerOptions, opts...) + } +} + +func NewGRPCRouter(logger mlogger.Logger, config *GRPCConfig, opts ...GRPCOption) (GRPC, error) { + options := &grpcimp.Options{} + for _, opt := range opts { + opt(options) + } + return grpcimp.NewRouter(logger, config, options) +} diff --git a/api/pkg/api/routers/gsresponse/response.go b/api/pkg/api/routers/gsresponse/response.go new file mode 100644 index 0000000..5cb377a --- /dev/null +++ b/api/pkg/api/routers/gsresponse/response.go @@ -0,0 +1,149 @@ +package gsresponse + +import ( + "context" + "errors" + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +// Responder produces a response or a gRPC status error when executed. +type Responder[T any] func(ctx context.Context) (*T, error) + +func message(err error) string { + if err == nil { + return "" + } + return err.Error() +} + +func Success[T any](resp *T) Responder[T] { + return func(context.Context) (*T, error) { + return resp, nil + } +} + +func Empty[T any]() Responder[T] { + return func(context.Context) (*T, error) { + return nil, nil + } +} + +func Error[T any](logger mlogger.Logger, service mservice.Type, code codes.Code, hint string, err error) Responder[T] { + return func(ctx context.Context) (*T, error) { + fields := []zap.Field{ + zap.String("service", string(service)), + zap.String("status_code", code.String()), + } + if hint != "" { + fields = append(fields, zap.String("error_hint", hint)) + } + if err != nil { + fields = append(fields, zap.Error(err)) + } + logFn := logger.Warn + switch code { + case codes.Internal, codes.DataLoss, codes.Unavailable: + logFn = logger.Error + } + logFn("gRPC request failed", fields...) + + msg := message(err) + switch { + case hint == "" && msg == "": + return nil, status.Error(code, code.String()) + case hint == "": + return nil, status.Error(code, msg) + case msg == "": + return nil, status.Error(code, hint) + default: + return nil, status.Error(code, fmt.Sprintf("%s: %s", hint, msg)) + } + } +} + +func Internal[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.Internal, "internal_error", err) +} + +func InvalidArgument[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.InvalidArgument, "invalid_argument", err) +} + +func NotFound[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.NotFound, "not_found", err) +} + +func Unauthorized[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.Unauthenticated, "unauthorized", err) +} + +func PermissionDenied[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.PermissionDenied, "access_denied", err) +} + +func FailedPrecondition[T any](logger mlogger.Logger, service mservice.Type, hint string, err error) Responder[T] { + return Error[T](logger, service, codes.FailedPrecondition, hint, err) +} + +func Conflict[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.Aborted, "conflict", err) +} + +func DeadlineExceeded[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.DeadlineExceeded, "deadline_exceeded", err) +} + +func Unavailable[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.Unavailable, "service_unavailable", err) +} + +func Unimplemented[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.Unimplemented, "not_implemented", err) +} + +func AlreadyExists[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + return Error[T](logger, service, codes.AlreadyExists, "already_exists", err) +} + +func Auto[T any](logger mlogger.Logger, service mservice.Type, err error) Responder[T] { + switch { + case err == nil: + return Empty[T]() + case errors.Is(err, merrors.ErrInvalidArg): + return InvalidArgument[T](logger, service, err) + case errors.Is(err, merrors.ErrAccessDenied): + return PermissionDenied[T](logger, service, err) + case errors.Is(err, merrors.ErrNoData): + return NotFound[T](logger, service, err) + case errors.Is(err, merrors.ErrUnauthorized): + return Unauthorized[T](logger, service, err) + case errors.Is(err, merrors.ErrDataConflict): + return Conflict[T](logger, service, err) + default: + return Internal[T](logger, service, err) + } +} + +func Execute[T any](ctx context.Context, responder Responder[T]) (*T, error) { + if responder == nil { + return nil, status.Error(codes.Internal, "missing responder") + } + return responder(ctx) +} + +func Unary[TReq any, TResp any](logger mlogger.Logger, service mservice.Type, handler func(context.Context, *TReq) Responder[TResp]) func(context.Context, *TReq) (*TResp, error) { + return func(ctx context.Context, req *TReq) (*TResp, error) { + if handler == nil { + return nil, status.Error(codes.Internal, "missing handler") + } + responder := handler(ctx, req) + return Execute(ctx, responder) + } +} diff --git a/api/pkg/api/routers/gsresponse/response_test.go b/api/pkg/api/routers/gsresponse/response_test.go new file mode 100644 index 0000000..dba8d04 --- /dev/null +++ b/api/pkg/api/routers/gsresponse/response_test.go @@ -0,0 +1,75 @@ +package gsresponse + +import ( + "context" + "errors" + "fmt" + "testing" + + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mservice" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" +) + +type testRequest struct { + Value string +} + +type testResponse struct { + Result string +} + +func TestUnarySuccess(t *testing.T) { + logger := zap.NewNop() + handler := func(ctx context.Context, req *testRequest) Responder[testResponse] { + require.NotNil(t, req) + require.Equal(t, "hello", req.Value) + resp := &testResponse{Result: "ok"} + return Success(resp) + } + + unary := Unary[testRequest, testResponse](logger, mservice.Type("test"), handler) + resp, err := unary(context.Background(), &testRequest{Value: "hello"}) + require.NoError(t, err) + require.NotNil(t, resp) + require.Equal(t, "ok", resp.Result) +} + +func TestAutoMappings(t *testing.T) { + logger := zap.NewNop() + service := mservice.Type("test") + + tests := []struct { + name string + err error + code codes.Code + }{ + {"invalid_argument", merrors.InvalidArgument("bad"), codes.InvalidArgument}, + {"access_denied", merrors.AccessDenied("object", "action", primitive.NilObjectID), codes.PermissionDenied}, + {"not_found", merrors.NoData("missing"), codes.NotFound}, + {"unauthorized", fmt.Errorf("%w: %s", merrors.ErrUnauthorized, "bad"), codes.Unauthenticated}, + {"conflict", merrors.DataConflict("conflict"), codes.Aborted}, + } + + for _, tc := range tests { + t.Run(tc.name, func(t *testing.T) { + responder := Auto[testResponse](logger, service, tc.err) + _, err := responder(context.Background()) + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, tc.code, st.Code()) + }) + } + + responder := Auto[testResponse](logger, service, errors.New("boom")) + _, err := responder(context.Background()) + require.Error(t, err) + st, ok := status.FromError(err) + require.True(t, ok) + require.Equal(t, codes.Internal, st.Code()) +} diff --git a/api/pkg/api/routers/health.go b/api/pkg/api/routers/health.go new file mode 100644 index 0000000..70e54c2 --- /dev/null +++ b/api/pkg/api/routers/health.go @@ -0,0 +1,17 @@ +package routers + +import ( + "github.com/go-chi/chi/v5" + "github.com/tech/sendico/pkg/api/routers/health" + "github.com/tech/sendico/pkg/api/routers/internal/healthimp" + "github.com/tech/sendico/pkg/mlogger" +) + +type Health interface { + SetStatus(status health.ServiceStatus) + Finish() +} + +func NewHealthRouter(logger mlogger.Logger, router chi.Router, endpoint string) (Health, error) { + return healthimp.NewRouter(logger, router, endpoint), nil +} diff --git a/api/pkg/api/routers/health/status.go b/api/pkg/api/routers/health/status.go new file mode 100644 index 0000000..9768bea --- /dev/null +++ b/api/pkg/api/routers/health/status.go @@ -0,0 +1,10 @@ +package health + +type ServiceStatus string + +const ( + SSCreated ServiceStatus = "created" + SSStarting ServiceStatus = "starting" + SSRunning ServiceStatus = "ok" + SSTerminating ServiceStatus = "deactivating" +) diff --git a/api/pkg/api/routers/internal/grpcimp/config.go b/api/pkg/api/routers/internal/grpcimp/config.go new file mode 100644 index 0000000..0833df6 --- /dev/null +++ b/api/pkg/api/routers/internal/grpcimp/config.go @@ -0,0 +1,18 @@ +package grpcimp + +type Config struct { + Network string `yaml:"network"` + Address string `yaml:"address"` + EnableReflection bool `yaml:"enable_reflection"` + EnableHealth bool `yaml:"enable_health"` + MaxRecvMsgSize int `yaml:"max_recv_msg_size"` + MaxSendMsgSize int `yaml:"max_send_msg_size"` + TLS *TLSConfig `yaml:"tls"` +} + +type TLSConfig struct { + CertFile string `yaml:"cert_file"` + KeyFile string `yaml:"key_file"` + CAFile string `yaml:"ca_file"` + RequireClientCert bool `yaml:"require_client_cert"` +} diff --git a/api/pkg/api/routers/internal/grpcimp/metrics.go b/api/pkg/api/routers/internal/grpcimp/metrics.go new file mode 100644 index 0000000..76a38c5 --- /dev/null +++ b/api/pkg/api/routers/internal/grpcimp/metrics.go @@ -0,0 +1,103 @@ +package grpcimp + +import ( + "context" + "strings" + "sync" + "time" + + "github.com/prometheus/client_golang/prometheus" + "google.golang.org/grpc" + "google.golang.org/grpc/status" +) + +var ( + metricsOnce sync.Once + grpcServerRequestsTotal *prometheus.CounterVec + grpcServerLatency *prometheus.HistogramVec +) + +func initPrometheusMetrics() { + metricsOnce.Do(func() { + grpcServerRequestsTotal = prometheus.NewCounterVec( + prometheus.CounterOpts{ + Name: "grpc_server_requests_total", + Help: "Total number of gRPC requests handled by the server.", + }, + []string{"grpc_service", "grpc_method", "grpc_type", "grpc_code"}, + ) + + grpcServerLatency = prometheus.NewHistogramVec( + prometheus.HistogramOpts{ + Name: "grpc_server_handling_seconds", + Help: "Duration of gRPC requests handled by the server.", + Buckets: prometheus.DefBuckets, + }, + []string{"grpc_service", "grpc_method", "grpc_type", "grpc_code"}, + ) + + prometheus.MustRegister(grpcServerRequestsTotal, grpcServerLatency) + }) +} + +func prometheusUnaryInterceptor() grpc.UnaryServerInterceptor { + initPrometheusMetrics() + + return func(ctx context.Context, req interface{}, info *grpc.UnaryServerInfo, handler grpc.UnaryHandler) (interface{}, error) { + start := time.Now() + resp, err := handler(ctx, req) + + recordMetrics(info.FullMethod, "unary", time.Since(start), err) + return resp, err + } +} + +func prometheusStreamInterceptor() grpc.StreamServerInterceptor { + initPrometheusMetrics() + + return func(srv interface{}, ss grpc.ServerStream, info *grpc.StreamServerInfo, handler grpc.StreamHandler) error { + start := time.Now() + err := handler(srv, ss) + + recordMetrics(info.FullMethod, streamType(info), time.Since(start), err) + return err + } +} + +func streamType(info *grpc.StreamServerInfo) string { + if info == nil { + return "stream" + } + if info.IsServerStream && info.IsClientStream { + return "bidi" + } + if info.IsServerStream { + return "server_stream" + } + if info.IsClientStream { + return "client_stream" + } + return "stream" +} + +func recordMetrics(fullMethod string, callType string, duration time.Duration, err error) { + service, method := splitMethod(fullMethod) + code := status.Code(err).String() + + grpcServerRequestsTotal.WithLabelValues(service, method, callType, code).Inc() + grpcServerLatency.WithLabelValues(service, method, callType, code).Observe(duration.Seconds()) +} + +func splitMethod(fullMethod string) (string, string) { + if fullMethod == "" { + return "unknown", "unknown" + } + if fullMethod[0] == '/' { + fullMethod = fullMethod[1:] + } + parts := strings.Split(fullMethod, "/") + if len(parts) < 2 { + return fullMethod, "unknown" + } + return parts[0], parts[1] +} diff --git a/api/pkg/api/routers/internal/grpcimp/options.go b/api/pkg/api/routers/internal/grpcimp/options.go new file mode 100644 index 0000000..fe83fa8 --- /dev/null +++ b/api/pkg/api/routers/internal/grpcimp/options.go @@ -0,0 +1,14 @@ +package grpcimp + +import ( + "net" + + "google.golang.org/grpc" +) + +type Options struct { + UnaryInterceptors []grpc.UnaryServerInterceptor + StreamInterceptors []grpc.StreamServerInterceptor + ServerOptions []grpc.ServerOption + Listener net.Listener +} diff --git a/api/pkg/api/routers/internal/grpcimp/router.go b/api/pkg/api/routers/internal/grpcimp/router.go new file mode 100644 index 0000000..cf34e8b --- /dev/null +++ b/api/pkg/api/routers/internal/grpcimp/router.go @@ -0,0 +1,293 @@ +package grpcimp + +import ( + "context" + "crypto/tls" + "crypto/x509" + "errors" + "net" + "os" + "sync" + + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/health" + healthpb "google.golang.org/grpc/health/grpc_health_v1" + "google.golang.org/grpc/reflection" +) + +type routerError string + +func (e routerError) Error() string { + return string(e) +} + +type routerErrorWithCause struct { + message string + cause error +} + +func (e *routerErrorWithCause) Error() string { + if e == nil { + return "" + } + if e.cause == nil { + return e.message + } + return e.message + ": " + e.cause.Error() +} + +func (e *routerErrorWithCause) Unwrap() error { + if e == nil { + return nil + } + return e.cause +} + +func newRouterErrorWithCause(message string, cause error) error { + return &routerErrorWithCause{ + message: message, + cause: cause, + } +} + +const ( + errMsgAlreadyStarted = "grpc router already started" + errMsgListenFailed = "failed to listen on requested address" + errMsgNilContext = "nil context" + errMsgTLSMissingCertAndKey = "tls configuration requires cert_file and key_file" + errMsgLoadServerCertificate = "failed to load server certificate" + errMsgReadCAFile = "failed to read CA file" + errMsgAppendCACertificates = "failed to append CA certificates" + errMsgClientCertRequiresCAFile = "client certificate verification requested but ca_file is empty" +) + +var ( + errAlreadyStarted = routerError(errMsgAlreadyStarted) + errNilContext = routerError(errMsgNilContext) + errTLSMissingCertAndKey = routerError(errMsgTLSMissingCertAndKey) + errAppendCACertificates = routerError(errMsgAppendCACertificates) + errClientCertRequiresCAFile = routerError(errMsgClientCertRequiresCAFile) +) + +type Router struct { + logger mlogger.Logger + config Config + server *grpc.Server + listener net.Listener + options *Options + mu sync.RWMutex + started bool + serveErr chan error + healthSrv *health.Server +} + +func NewRouter(logger mlogger.Logger, cfg *Config, opts *Options) (*Router, error) { + if cfg == nil { + cfg = &Config{} + } + if opts == nil { + opts = &Options{} + } + + network := cfg.Network + if network == "" { + network = "tcp" + } + address := cfg.Address + if address == "" { + address = ":0" + } + + listener := opts.Listener + var err error + if listener == nil { + listener, err = net.Listen(network, address) + if err != nil { + return nil, newRouterErrorWithCause(errMsgListenFailed, err) + } + } + + serverOpts := make([]grpc.ServerOption, 0, len(opts.ServerOptions)+4) + serverOpts = append(serverOpts, opts.ServerOptions...) + + if cfg.MaxRecvMsgSize > 0 { + serverOpts = append(serverOpts, grpc.MaxRecvMsgSize(cfg.MaxRecvMsgSize)) + } + if cfg.MaxSendMsgSize > 0 { + serverOpts = append(serverOpts, grpc.MaxSendMsgSize(cfg.MaxSendMsgSize)) + } + + if creds, err := configureTLS(cfg.TLS); err != nil { + return nil, err + } else if creds != nil { + serverOpts = append(serverOpts, grpc.Creds(creds)) + } + + unaryInterceptors := append([]grpc.UnaryServerInterceptor{prometheusUnaryInterceptor()}, opts.UnaryInterceptors...) + streamInterceptors := append([]grpc.StreamServerInterceptor{prometheusStreamInterceptor()}, opts.StreamInterceptors...) + + if len(unaryInterceptors) > 0 { + serverOpts = append(serverOpts, grpc.ChainUnaryInterceptor(unaryInterceptors...)) + } + if len(streamInterceptors) > 0 { + serverOpts = append(serverOpts, grpc.ChainStreamInterceptor(streamInterceptors...)) + } + + srv := grpc.NewServer(serverOpts...) + r := &Router{ + logger: logger.Named("grpc"), + config: *cfg, + server: srv, + listener: listener, + options: opts, + serveErr: make(chan error, 1), + } + + if cfg.EnableReflection { + reflection.Register(srv) + } + if cfg.EnableHealth { + r.healthSrv = health.NewServer() + r.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING) + healthpb.RegisterHealthServer(srv, r.healthSrv) + } + + return r, nil +} + +func (r *Router) Register(registration func(grpc.ServiceRegistrar)) error { + r.mu.Lock() + defer r.mu.Unlock() + if r.started { + return errAlreadyStarted + } + + registration(r.server) + return nil +} + +func (r *Router) Start(ctx context.Context) error { + if ctx == nil { + return errNilContext + } + + r.mu.Lock() + if r.started { + r.mu.Unlock() + return errAlreadyStarted + } + r.started = true + r.mu.Unlock() + + if r.healthSrv != nil { + r.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_SERVING) + } + + go func() { + <-ctx.Done() + r.logger.Info("Context cancelled, stopping gRPC server") + r.server.GracefulStop() + }() + + go func() { + err := r.server.Serve(r.listener) + if err != nil && !errors.Is(err, grpc.ErrServerStopped) { + select { + case r.serveErr <- err: + default: + r.logger.Error("Failed to report gRPC serve error", zap.Error(err)) + } + } + close(r.serveErr) + }() + + r.logger.Info("gRPC server started", zap.String("network", r.listener.Addr().Network()), zap.String("address", r.listener.Addr().String())) + return nil +} + +func (r *Router) Finish(ctx context.Context) error { + if ctx == nil { + return errNilContext + } + + r.mu.RLock() + started := r.started + r.mu.RUnlock() + if !started { + return nil + } + + if r.healthSrv != nil { + r.healthSrv.SetServingStatus("", healthpb.HealthCheckResponse_NOT_SERVING) + } + + done := make(chan struct{}) + go func() { + r.server.GracefulStop() + close(done) + }() + + select { + case <-done: + case <-ctx.Done(): + r.logger.Warn("Graceful stop timed out, forcing stop", zap.Error(ctx.Err())) + r.server.Stop() + return ctx.Err() + } + + if err, ok := <-r.serveErr; ok { + return err + } + return nil +} + +func (r *Router) Addr() net.Addr { + return r.listener.Addr() +} + +func (r *Router) Done() <-chan error { + return r.serveErr +} + +func configureTLS(cfg *TLSConfig) (credentials.TransportCredentials, error) { + if cfg == nil { + return nil, nil + } + + if cfg.CertFile == "" || cfg.KeyFile == "" { + return nil, errTLSMissingCertAndKey + } + + certificate, err := tls.LoadX509KeyPair(cfg.CertFile, cfg.KeyFile) + if err != nil { + return nil, newRouterErrorWithCause(errMsgLoadServerCertificate, err) + } + + tlsCfg := &tls.Config{ + Certificates: []tls.Certificate{certificate}, + MinVersion: tls.VersionTLS12, + } + + if cfg.CAFile != "" { + caPem, err := os.ReadFile(cfg.CAFile) + if err != nil { + return nil, newRouterErrorWithCause(errMsgReadCAFile, err) + } + + certPool := x509.NewCertPool() + if ok := certPool.AppendCertsFromPEM(caPem); !ok { + return nil, errAppendCACertificates + } + tlsCfg.ClientCAs = certPool + if cfg.RequireClientCert { + tlsCfg.ClientAuth = tls.RequireAndVerifyClientCert + } + } else if cfg.RequireClientCert { + return nil, errClientCertRequiresCAFile + } + + return credentials.NewTLS(tlsCfg), nil +} diff --git a/api/pkg/api/routers/internal/grpcimp/router_test.go b/api/pkg/api/routers/internal/grpcimp/router_test.go new file mode 100644 index 0000000..a7a3e07 --- /dev/null +++ b/api/pkg/api/routers/internal/grpcimp/router_test.go @@ -0,0 +1,150 @@ +package grpcimp + +import ( + "context" + "testing" + "time" + + "github.com/stretchr/testify/require" + "go.uber.org/zap" + "google.golang.org/grpc" + "google.golang.org/grpc/test/bufconn" +) + +const bufconnSize = 1024 * 1024 + +func newBufferedListener(t *testing.T) *bufconn.Listener { + t.Helper() + + listener := bufconn.Listen(bufconnSize) + t.Cleanup(func() { + listener.Close() + }) + + return listener +} + +func newTestRouter(t *testing.T, cfg *Config) *Router { + t.Helper() + + logger := zap.NewNop() + if cfg == nil { + cfg = &Config{} + } + + router, err := NewRouter(logger, cfg, &Options{Listener: newBufferedListener(t)}) + require.NoError(t, err) + + return router +} + +func TestRouterStartAndFinish(t *testing.T) { + router := newTestRouter(t, &Config{}) + + doneCh := router.Done() + require.NotNil(t, doneCh) + + require.NoError(t, router.Register(func(grpc.ServiceRegistrar) {})) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + require.NoError(t, router.Start(ctx)) + + addr := router.Addr() + require.NotNil(t, addr) + require.NotEmpty(t, addr.String()) + + finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second) + defer finishCancel() + + require.NoError(t, router.Finish(finishCtx)) + + select { + case err, ok := <-doneCh: + if ok { + require.NoError(t, err) + } + case <-time.After(time.Second): + t.Fatal("timed out waiting for done channel") + } +} + +func TestRouterRejectsRegistrationAfterStart(t *testing.T) { + router := newTestRouter(t, &Config{}) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + require.NoError(t, router.Start(ctx)) + + doneCh := router.Done() + + err := router.Register(func(grpc.ServiceRegistrar) {}) + require.ErrorIs(t, err, errAlreadyStarted) + + finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second) + defer finishCancel() + + require.NoError(t, router.Finish(finishCtx)) + + select { + case <-doneCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for done channel") + } +} + +func TestRouterStartOnlyOnce(t *testing.T) { + router := newTestRouter(t, &Config{}) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + require.NoError(t, router.Start(ctx)) + require.ErrorIs(t, router.Start(ctx), errAlreadyStarted) + + doneCh := router.Done() + + finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second) + defer finishCancel() + + require.NoError(t, router.Finish(finishCtx)) + + select { + case <-doneCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for done channel") + } +} + +func TestRouterUsesProvidedListener(t *testing.T) { + logger := zap.NewNop() + listener := newBufferedListener(t) + + cfg := &Config{} + router, err := NewRouter(logger, cfg, &Options{Listener: listener}) + require.NoError(t, err) + + actualListener, ok := router.listener.(*bufconn.Listener) + require.True(t, ok) + require.Same(t, listener, actualListener) + + ctx, cancel := context.WithCancel(context.Background()) + defer cancel() + + require.NoError(t, router.Start(ctx)) + + doneCh := router.Done() + + finishCtx, finishCancel := context.WithTimeout(context.Background(), time.Second) + defer finishCancel() + + require.NoError(t, router.Finish(finishCtx)) + + select { + case <-doneCh: + case <-time.After(time.Second): + t.Fatal("timed out waiting for done channel") + } +} diff --git a/api/pkg/api/routers/internal/healthimp/health.go b/api/pkg/api/routers/internal/healthimp/health.go new file mode 100644 index 0000000..b9ad77e --- /dev/null +++ b/api/pkg/api/routers/internal/healthimp/health.go @@ -0,0 +1,45 @@ +package healthimp + +import ( + "net/http" + + "github.com/go-chi/chi/v5" + "github.com/tech/sendico/pkg/api/routers/health" + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" +) + +type Router struct { + logger mlogger.Logger + status *Status +} + +func (hr *Router) SetStatus(status health.ServiceStatus) { + hr.status.setStatus(status) + hr.logger.Info("New status set", zap.String("status", string(status))) +} + +func (hr *Router) Finish() { + hr.status.Finish() + hr.logger.Debug("Stopped") +} + +func (hr *Router) handle(w http.ResponseWriter, r *http.Request) { + hr.status.healthHandler()(w, r) +} + +func NewRouter(logger mlogger.Logger, router chi.Router, endpoint string) *Router { + hr := Router{ + logger: logger.Named("health_check"), + } + hr.status = StatusHandler(hr.logger) + + logger.Debug("Installing healthcheck middleware...") + router.Group(func(r chi.Router) { + ep := endpoint + "/health" + r.Get(ep, hr.handle) + logger.Info("Health handler installed", zap.String("endpoint", ep)) + }) + + return &hr +} diff --git a/api/pkg/api/routers/internal/healthimp/status.go b/api/pkg/api/routers/internal/healthimp/status.go new file mode 100644 index 0000000..94a09ba --- /dev/null +++ b/api/pkg/api/routers/internal/healthimp/status.go @@ -0,0 +1,38 @@ +package healthimp + +import ( + "net/http" + + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/api/routers/health" + "github.com/tech/sendico/pkg/mlogger" +) + +type Status struct { + logger mlogger.Logger + status health.ServiceStatus +} + +func (hs *Status) healthHandler() http.HandlerFunc { + return response.Ok(hs.logger, struct { + Status health.ServiceStatus `json:"status"` + }{ + hs.status, + }) +} + +func (hr *Status) Finish() { + hr.logger.Info("Finished") +} + +func (hs *Status) setStatus(status health.ServiceStatus) { + hs.status = status +} + +func StatusHandler(logger mlogger.Logger) *Status { + hs := Status{ + status: health.SSCreated, + logger: logger.Named("status"), + } + return &hs +} diff --git a/api/pkg/api/routers/internal/messagingimp/consumer.go b/api/pkg/api/routers/internal/messagingimp/consumer.go new file mode 100644 index 0000000..3423223 --- /dev/null +++ b/api/pkg/api/routers/internal/messagingimp/consumer.go @@ -0,0 +1,66 @@ +package messagingimp + +import ( + "context" + + "github.com/tech/sendico/pkg/messaging" + mb "github.com/tech/sendico/pkg/messaging/broker" + me "github.com/tech/sendico/pkg/messaging/envelope" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +type ChannelConsumer struct { + logger mlogger.Logger + broker mb.Broker + event model.NotificationEvent + ch <-chan me.Envelope + ctx context.Context + cancel context.CancelFunc +} + +func (c *ChannelConsumer) ConsumeMessages(handleFunc messaging.MessageHandlerT) error { + c.logger.Info("Message consumer is ready") + for { + select { + case msg := <-c.ch: + if msg == nil { // nil message indicates the channel was closed + c.logger.Info("Consumer shutting down") + return nil + } + if err := handleFunc(c.ctx, msg); err != nil { + c.logger.Warn("Error processing message", zap.Error(err)) + } + case <-c.ctx.Done(): + c.logger.Info("Context done, shutting down") + return c.ctx.Err() + } + } +} + +func (c *ChannelConsumer) Close() { + c.logger.Info("Shutting down...") + c.cancel() + if err := c.broker.Unsubscribe(c.event, c.ch); err != nil { + c.logger.Warn("Failed to unsubscribe", zap.Error(err)) + } +} + +func NewConsumer(logger mlogger.Logger, broker mb.Broker, event model.NotificationEvent) (*ChannelConsumer, error) { + ctx, cancel := context.WithCancel(context.Background()) + ch, err := broker.Subscribe(event) + if err != nil { + logger.Warn("Failed to create channel consumer", zap.Error(err), zap.String("topic", event.ToString())) + cancel() // Ensure resources are released properly + return nil, err + } + return &ChannelConsumer{ + logger: logger.Named("consumer").Named(event.ToString()), + broker: broker, + event: event, + ch: ch, + ctx: ctx, + cancel: cancel, + }, nil +} diff --git a/api/pkg/api/routers/internal/messagingimp/messsaging.go b/api/pkg/api/routers/internal/messagingimp/messsaging.go new file mode 100644 index 0000000..ca0d9d7 --- /dev/null +++ b/api/pkg/api/routers/internal/messagingimp/messsaging.go @@ -0,0 +1,67 @@ +package messagingimp + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/messaging" + mb "github.com/tech/sendico/pkg/messaging/broker" + notifications "github.com/tech/sendico/pkg/messaging/notifications/processor" + mip "github.com/tech/sendico/pkg/messaging/producer" + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" +) + +type MessagingRouter struct { + logger mlogger.Logger + messaging mb.Broker + consumers []messaging.Consumer + producer messaging.Producer +} + +func (mr *MessagingRouter) consumeMessages(c messaging.Consumer, processor notifications.EnvelopeProcessor) { + if err := c.ConsumeMessages(processor.Process); err != nil { + if !errors.Is(err, context.Canceled) { + mr.logger.Warn("Error consuming messages", zap.Error(err), zap.String("event", processor.GetSubject().ToString())) + } else { + mr.logger.Info("Finishing as context has been cancelled", zap.String("event", processor.GetSubject().ToString())) + } + } +} + +func (mr *MessagingRouter) Consumer(processor notifications.EnvelopeProcessor) error { + c, err := NewConsumer(mr.logger, mr.messaging, processor.GetSubject()) + if err != nil { + mr.logger.Warn("Failed to register message consumer", zap.Error(err), zap.String("event", processor.GetSubject().ToString())) + return err + } + mr.consumers = append(mr.consumers, c) + go mr.consumeMessages(c, processor) + return nil +} + +func (mr *MessagingRouter) Finish() { + mr.logger.Info("Closing consumer channels") + for _, consumer := range mr.consumers { + consumer.Close() + } +} + +func (mr *MessagingRouter) Producer() messaging.Producer { + return mr.producer +} + +func NewMessagingRouterImp(logger mlogger.Logger, config *messaging.Config) (*MessagingRouter, error) { + l := logger.Named("messaging") + broker, err := messaging.CreateMessagingBroker(l, config) + if err != nil { + l.Error("Failed to create messaging broker", zap.Error(err), zap.String("broker", string(config.Driver))) + return nil, err + } + return &MessagingRouter{ + logger: l, + messaging: broker, + producer: mip.NewProducer(logger, broker), + consumers: make([]messaging.Consumer, 0), + }, nil +} diff --git a/api/pkg/api/routers/messaging.go b/api/pkg/api/routers/messaging.go new file mode 100644 index 0000000..ba5897f --- /dev/null +++ b/api/pkg/api/routers/messaging.go @@ -0,0 +1,16 @@ +package routers + +import ( + "github.com/tech/sendico/pkg/api/routers/internal/messagingimp" + "github.com/tech/sendico/pkg/messaging" + "github.com/tech/sendico/pkg/mlogger" +) + +type Messaging interface { + messaging.Register + Finish() +} + +func NewMessagingRouter(logger mlogger.Logger, config *messaging.Config) (Messaging, error) { + return messagingimp.NewMessagingRouterImp(logger, config) +} diff --git a/api/pkg/auth/USAGE.md b/api/pkg/auth/USAGE.md new file mode 100644 index 0000000..aab79ae --- /dev/null +++ b/api/pkg/auth/USAGE.md @@ -0,0 +1,202 @@ +# Auth.Indexable Usage Guide + +## Secure Reordering with Permission Checking + +The `auth.Indexable` implementation adds **permission checking** to the generic reordering functionality using `EnforceBatch`. + +- **Core Implementation**: `api/pkg/auth/indexable.go` - generic implementation with permission checking +- **Project Factory**: `api/pkg/auth/project_indexable.go` - convenient factory for projects +- **Key Feature**: Uses `EnforceBatch` to check permissions for all affected objects + +## How It Works + +### Permission Checking Flow +1. **Get current object** to find its index +2. **Determine affected objects** that will be shifted during reordering +3. **Check permissions** using `EnforceBatch` for all affected objects + target object +4. **Verify all permissions** - if any object lacks update permission, return error +5. **Proceed with reordering** only if all permissions are granted + +### Key Differences from Basic Indexable +- **Additional parameter**: `accountRef` for permission checking +- **Permission validation**: All affected objects must have `ActionUpdate` permission +- **Security**: Prevents unauthorized reordering that could affect other users' data + +## Usage + +### 1. Using the Generic Auth.Indexable Implementation + +```go +import "github.com/tech/sendico/pkg/auth" + +// For any type that embeds model.Indexable, define helper functions: +createEmpty := func() *YourType { + return &YourType{} +} + +getIndexable := func(obj *YourType) *model.Indexable { + return &obj.Indexable +} + +// Create auth.IndexableDB with enforcer +indexableDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable) + +// Use with account reference for permission checking +err := indexableDB.Reorder(ctx, accountRef, objectID, newIndex, filter) +``` + +### 2. Using the Project Factory (Recommended for Projects) + +```go +import "github.com/tech/sendico/pkg/auth" + +// Create auth.ProjectIndexableDB (automatically applies org filter) +projectDB := auth.NewProjectIndexableDB(repo, logger, enforcer, organizationRef) + +// Reorder project with permission checking +err := projectDB.Reorder(ctx, accountRef, projectID, newIndex, repository.Query()) + +// Reorder with additional filters (combined with org filter) +additionalFilter := repository.Query().Comparison(repository.Field("state"), builder.Eq, "active") +err := projectDB.Reorder(ctx, accountRef, projectID, newIndex, additionalFilter) +``` + +## Examples for Different Types + +### Project Auth.IndexableDB +```go +createEmpty := func() *model.Project { + return &model.Project{} +} + +getIndexable := func(p *model.Project) *model.Indexable { + return &p.Indexable +} + +projectDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable) +orgFilter := repository.OrgFilter(organizationRef) +projectDB.Reorder(ctx, accountRef, projectID, 2, orgFilter) +``` + +### Status Auth.IndexableDB +```go +createEmpty := func() *model.Status { + return &model.Status{} +} + +getIndexable := func(s *model.Status) *model.Indexable { + return &s.Indexable +} + +statusDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable) +projectFilter := repository.Query().Comparison(repository.Field("projectRef"), builder.Eq, projectRef) +statusDB.Reorder(ctx, accountRef, statusID, 1, projectFilter) +``` + +### Task Auth.IndexableDB +```go +createEmpty := func() *model.Task { + return &model.Task{} +} + +getIndexable := func(t *model.Task) *model.Indexable { + return &t.Indexable +} + +taskDB := auth.NewIndexableDB(repo, logger, enforcer, createEmpty, getIndexable) +statusFilter := repository.Query().Comparison(repository.Field("statusRef"), builder.Eq, statusRef) +taskDB.Reorder(ctx, accountRef, taskID, 3, statusFilter) +``` + +## Permission Checking Details + +### What Gets Checked +When reordering an object from index `A` to index `B`: + +1. **Target object** - the object being moved +2. **Affected objects** - all objects whose indices will be shifted: + - Moving down: objects between `A+1` and `B` (shifted up by -1) + - Moving up: objects between `B` and `A-1` (shifted down by +1) + +### Permission Requirements +- **Action**: `model.ActionUpdate` +- **Scope**: All affected objects must be `PermissionBoundStorable` +- **Result**: If any object lacks permission, the entire operation fails + +### Error Handling +```go +// Permission denied error +if err != nil { + if strings.Contains(err.Error(), "accessDenied") { + // Handle permission denied + } +} +``` + +## Security Benefits + +### ✅ **Comprehensive Permission Checking** +- Checks permissions for **all affected objects**, not just the target +- Prevents unauthorized reordering that could affect other users' data +- Uses efficient `EnforceBatch` for bulk permission checking + +### ✅ **Type Safety** +- Generic implementation works with any `Indexable` struct +- Compile-time type checking +- No runtime type assertions + +### ✅ **Flexible Filtering** +- Single `builder.Query` parameter for scoping +- Can combine organization filters with additional criteria +- Project factory automatically applies organization filtering + +### ✅ **Clean Architecture** +- Separates permission logic from reordering logic +- Easy to test with mock enforcers +- Follows existing auth patterns + +## Testing + +### Mock Enforcer Setup +```go +mockEnforcer := &MockEnforcer{} + +// Grant all permissions +permissions := map[primitive.ObjectID]bool{ + objectID1: true, + objectID2: true, +} +mockEnforcer.On("EnforceBatch", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(permissions, nil) + +// Deny specific permission +permissions[objectID2] = false +mockEnforcer.On("EnforceBatch", mock.Anything, mock.Anything, mock.Anything, mock.Anything). + Return(permissions, nil) +``` + +### Test Scenarios +- ✅ **Permission granted** - reordering succeeds +- ❌ **Permission denied** - reordering fails with access denied error +- 🔄 **No change needed** - early return, minimal permission checking +- 🏢 **Organization filtering** - automatic org scope for projects + +## Comparison: Basic vs Auth.Indexable + +| Feature | Basic Indexable | Auth.Indexable | +|---------|----------------|----------------| +| Permission checking | ❌ No | ✅ Yes | +| Account parameter | ❌ No | ✅ Required | +| Security | ❌ None | ✅ Comprehensive | +| Performance | ✅ Fast | ⚠️ Slower (permission checks) | +| Use case | Internal operations | User-facing operations | + +## Best Practices + +1. **Use Auth.Indexable** for user-facing reordering operations +2. **Use Basic Indexable** for internal/system operations +3. **Always provide account reference** for proper permission checking +4. **Test permission scenarios** thoroughly with mock enforcers +5. **Handle permission errors** gracefully in user interfaces + +That's it! **Secure, type-safe reordering** with comprehensive permission checking using `EnforceBatch`. \ No newline at end of file diff --git a/api/pkg/auth/anyobject/anyobject.go b/api/pkg/auth/anyobject/anyobject.go new file mode 100644 index 0000000..124da0f --- /dev/null +++ b/api/pkg/auth/anyobject/anyobject.go @@ -0,0 +1,3 @@ +package anyobject + +const ID = "*" diff --git a/api/pkg/auth/archivable.go b/api/pkg/auth/archivable.go new file mode 100644 index 0000000..e099fec --- /dev/null +++ b/api/pkg/auth/archivable.go @@ -0,0 +1,35 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// ArchivableDB implements archive operations with permission checking +type ArchivableDB[T model.PermissionBoundStorable] interface { + // SetArchived sets the archived status of an entity with permission checking + SetArchived(ctx context.Context, accountRef, objectRef primitive.ObjectID, archived bool) error + // IsArchived checks if an entity is archived with permission checking + IsArchived(ctx context.Context, accountRef, objectRef primitive.ObjectID) (bool, error) + + // Archive archives an entity with permission checking (sets archived to true) + Archive(ctx context.Context, accountRef, objectRef primitive.ObjectID) error + + // Unarchive unarchives an entity with permission checking (sets archived to false) + Unarchive(ctx context.Context, accountRef, objectRef primitive.ObjectID) error +} + +// NewArchivableDB creates a new auth.ArchivableDB instance +func NewArchivableDB[T model.PermissionBoundStorable]( + dbImp *template.DBImp[T], + logger mlogger.Logger, + enforcer Enforcer, + createEmpty func() T, + getArchivable func(T) model.Archivable, +) ArchivableDB[T] { + return newArchivableDBImp(dbImp, logger, enforcer, createEmpty, getArchivable) +} diff --git a/api/pkg/auth/archivableimp.go b/api/pkg/auth/archivableimp.go new file mode 100644 index 0000000..114e2ca --- /dev/null +++ b/api/pkg/auth/archivableimp.go @@ -0,0 +1,107 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// ArchivableDB implements archive operations with permission checking +type ArchivableDBImp[T model.PermissionBoundStorable] struct { + dbImp *template.DBImp[T] + logger mlogger.Logger + enforcer Enforcer + createEmpty func() T + getArchivable func(T) model.Archivable +} + +// NewArchivableDB creates a new auth.ArchivableDB instance +func newArchivableDBImp[T model.PermissionBoundStorable]( + dbImp *template.DBImp[T], + logger mlogger.Logger, + enforcer Enforcer, + createEmpty func() T, + getArchivable func(T) model.Archivable, +) ArchivableDB[T] { + return &ArchivableDBImp[T]{ + dbImp: dbImp, + logger: logger.Named("archivable"), + enforcer: enforcer, + createEmpty: createEmpty, + getArchivable: getArchivable, + } +} + +// SetArchived sets the archived status of an entity with permission checking +func (db *ArchivableDBImp[T]) SetArchived(ctx context.Context, accountRef, objectRef primitive.ObjectID, archived bool) error { + // Check permissions using enforceObject helper + if err := enforceObjectByRef(ctx, db.dbImp, db.enforcer, model.ActionUpdate, accountRef, objectRef); err != nil { + db.logger.Warn("Failed to enforce object permission", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived)) + return err + } + + // Get the object to check current archived status + obj := db.createEmpty() + if err := db.dbImp.Get(ctx, objectRef, obj); err != nil { + db.logger.Warn("Failed to get object for setting archived status", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived)) + return err + } + + // Extract archivable from the object + archivable := db.getArchivable(obj) + currentArchived := archivable.IsArchived() + if currentArchived == archived { + db.logger.Debug("No change needed - same archived status", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived)) + return nil // No change needed + } + + // Set the archived status + patch := repository.Patch().Set(repository.IsArchivedField(), archived) + if err := db.dbImp.Patch(ctx, objectRef, patch); err != nil { + db.logger.Warn("Failed to set archived status on object", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived)) + return err + } + + db.logger.Debug("Successfully set archived status on object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", archived)) + return nil +} + +// IsArchived checks if an entity is archived with permission checking +func (db *ArchivableDBImp[T]) IsArchived(ctx context.Context, accountRef, objectRef primitive.ObjectID) (bool, error) { + // // Check permissions using single Enforce + if err := enforceObjectByRef(ctx, db.dbImp, db.enforcer, model.ActionRead, accountRef, objectRef); err != nil { + db.logger.Debug("Permission denied for checking archived status", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionRead))) + return false, merrors.AccessDenied("read", "object", objectRef) + } + obj := db.createEmpty() + if err := db.dbImp.Get(ctx, objectRef, obj); err != nil { + db.logger.Warn("Failed to get object for checking archived status", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return false, err + } + archivable := db.getArchivable(obj) + return archivable.IsArchived(), nil +} + +// Archive archives an entity with permission checking (sets archived to true) +func (db *ArchivableDBImp[T]) Archive(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { + return db.SetArchived(ctx, accountRef, objectRef, true) +} + +// Unarchive unarchives an entity with permission checking (sets archived to false) +func (db *ArchivableDBImp[T]) Unarchive(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { + return db.SetArchived(ctx, accountRef, objectRef, false) +} diff --git a/api/pkg/auth/config.go b/api/pkg/auth/config.go new file mode 100644 index 0000000..4ec922e --- /dev/null +++ b/api/pkg/auth/config.go @@ -0,0 +1,12 @@ +package auth + +import "github.com/tech/sendico/pkg/model" + +type EnforcerType string + +const ( + Casbin EnforcerType = "casbin" + Native EnforcerType = "native" +) + +type Config = model.DriverConfig[EnforcerType] diff --git a/api/pkg/auth/customizable/customizable.go b/api/pkg/auth/customizable/customizable.go new file mode 100644 index 0000000..ed48452 --- /dev/null +++ b/api/pkg/auth/customizable/customizable.go @@ -0,0 +1,8 @@ +package customizable + +import ( + "github.com/tech/sendico/pkg/model" +) + +type DB[T model.PermissionBoundStorable] interface { +} diff --git a/api/pkg/auth/customizable/manager.go b/api/pkg/auth/customizable/manager.go new file mode 100644 index 0000000..f26cbd9 --- /dev/null +++ b/api/pkg/auth/customizable/manager.go @@ -0,0 +1,8 @@ +package customizable + +import ( + "github.com/tech/sendico/pkg/model" +) + +type Manager[T model.PermissionBoundStorable] interface { +} diff --git a/api/pkg/auth/db.go b/api/pkg/auth/db.go new file mode 100644 index 0000000..a6667c6 --- /dev/null +++ b/api/pkg/auth/db.go @@ -0,0 +1,38 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type ProtectedDB[T model.PermissionBoundStorable] interface { + Create(ctx context.Context, accountRef, organizationRef primitive.ObjectID, object T) error + InsertMany(ctx context.Context, accountRef, organizationRef primitive.ObjectID, objects []T) error + Get(ctx context.Context, accountRef, objectRef primitive.ObjectID, result T) error + Update(ctx context.Context, accountRef primitive.ObjectID, object T) error + Delete(ctx context.Context, accountRef, objectRef primitive.ObjectID) error + DeleteCascadeAuth(ctx context.Context, accountRef, objectRef primitive.ObjectID) error + Patch(ctx context.Context, accountRef, objectRef primitive.ObjectID, patch builder.Patch) error + PatchMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, patch builder.Patch) (int, error) + Unprotected() template.DB[T] + ListIDs(ctx context.Context, action model.Action, accountRef primitive.ObjectID, query builder.Query) ([]primitive.ObjectID, error) +} + +func CreateDB[T model.PermissionBoundStorable]( + ctx context.Context, + l mlogger.Logger, + pdb policy.DB, + enforcer Enforcer, + collection mservice.Type, + db *mongo.Database, +) (ProtectedDB[T], error) { + return CreateDBImp[T](ctx, l, pdb, enforcer, collection, db) +} diff --git a/api/pkg/auth/dbab.go b/api/pkg/auth/dbab.go new file mode 100644 index 0000000..a72a4ea --- /dev/null +++ b/api/pkg/auth/dbab.go @@ -0,0 +1,51 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type AccountBoundDB[T model.AccountBoundStorable] interface { + Create(ctx context.Context, accountRef primitive.ObjectID, object T) error + Get(ctx context.Context, accountRef, objectRef primitive.ObjectID, result T) error + Update(ctx context.Context, accountRef primitive.ObjectID, object T) error + Patch(ctx context.Context, accountRef, objectRef primitive.ObjectID, patch builder.Patch) error + Delete(ctx context.Context, accountRef, objectRef primitive.ObjectID) error + DeleteMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query) error + FindOne(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, result T) error + ListIDs(ctx context.Context, accountRef primitive.ObjectID, query builder.Query) ([]primitive.ObjectID, error) + ListAccountBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID, query builder.Query) ([]model.AccountBoundStorable, error) +} + +func CreateAccountBound[T model.AccountBoundStorable]( + ctx context.Context, + logger mlogger.Logger, + pdb policy.DB, + enforcer Enforcer, + collection mservice.Type, + db *mongo.Database, +) (AccountBoundDB[T], error) { + logger = logger.Named("account_bound") + var policy model.PolicyDescription + if err := pdb.GetBuiltInPolicy(ctx, mservice.Organizations, &policy); err != nil { + logger.Warn("Failed to fetch organization policy description", zap.Error(err)) + return nil, err + } + res := &AccountBoundDBImp[T]{ + Logger: logger, + DBImp: template.Create[T](logger, collection, db), + Enforcer: enforcer, + PermissionRef: policy.ID, + Collection: collection, + } + return res, nil +} diff --git a/api/pkg/auth/dbimp.go b/api/pkg/auth/dbimp.go new file mode 100644 index 0000000..40e9c2a --- /dev/null +++ b/api/pkg/auth/dbimp.go @@ -0,0 +1,319 @@ +package auth + +import ( + "context" + "errors" + "fmt" + + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type ProtectedDBImp[T model.PermissionBoundStorable] struct { + DBImp *template.DBImp[T] + Enforcer Enforcer + PermissionRef primitive.ObjectID + Collection mservice.Type +} + +func (db *ProtectedDBImp[T]) enforce(ctx context.Context, action model.Action, object model.PermissionBoundStorable, accountRef, objectRef primitive.ObjectID) error { + res, err := db.Enforcer.Enforce(ctx, object.GetPermissionRef(), accountRef, object.GetOrganizationRef(), objectRef, action) + if err != nil { + db.DBImp.Logger.Warn("Failed to enforce permission", + zap.Error(err), mzap.ObjRef("permission_ref", object.GetPermissionRef()), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return err + } + if !res { + db.DBImp.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", object.GetPermissionRef()), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return merrors.AccessDenied(db.Collection, string(action), objectRef) + } + return nil +} + +func (db *ProtectedDBImp[T]) Create(ctx context.Context, accountRef, organizationRef primitive.ObjectID, object T) error { + db.DBImp.Logger.Debug("Attempting to create object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection))) + + if object.GetPermissionRef() == primitive.NilObjectID { + object.SetPermissionRef(db.PermissionRef) + } + object.SetOrganizationRef(organizationRef) + + if err := db.enforce(ctx, model.ActionCreate, object, accountRef, primitive.NilObjectID); err != nil { + return err + } + + if err := db.DBImp.Create(ctx, object); err != nil { + db.DBImp.Logger.Warn("Failed to create object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection))) + return err + } + + db.DBImp.Logger.Debug("Successfully created object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection))) + return nil +} + +func (db *ProtectedDBImp[T]) InsertMany(ctx context.Context, accountRef, organizationRef primitive.ObjectID, objects []T) error { + if len(objects) == 0 { + return nil + } + + db.DBImp.Logger.Debug("Attempting to insert many objects", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)), + zap.Int("count", len(objects))) + + // Set permission and organization refs for all objects and enforce permissions + for _, object := range objects { + if object.GetPermissionRef() == primitive.NilObjectID { + object.SetPermissionRef(db.PermissionRef) + } + object.SetOrganizationRef(organizationRef) + + if err := db.enforce(ctx, model.ActionCreate, object, accountRef, primitive.NilObjectID); err != nil { + return err + } + } + + if err := db.DBImp.InsertMany(ctx, objects); err != nil { + db.DBImp.Logger.Warn("Failed to insert many objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)), + zap.Int("count", len(objects))) + return err + } + + db.DBImp.Logger.Debug("Successfully inserted many objects", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), zap.String("collection", string(db.Collection)), + zap.Int("count", len(objects))) + return nil +} + +func (db *ProtectedDBImp[T]) enforceObject(ctx context.Context, action model.Action, accountRef, objectRef primitive.ObjectID) error { + l, err := db.ListIDs(ctx, action, accountRef, repository.IDFilter(objectRef)) + if err != nil { + db.DBImp.Logger.Warn("Error occured while checking access rights", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return err + } + if len(l) == 0 { + db.DBImp.Logger.Debug("Access denied", zap.String("action", string(action)), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return merrors.AccessDenied(db.Collection, string(action), objectRef) + } + return nil +} + +func (db *ProtectedDBImp[T]) Get(ctx context.Context, accountRef, objectRef primitive.ObjectID, result T) error { + db.DBImp.Logger.Debug("Attempting to get object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + + if err := db.enforceObject(ctx, model.ActionRead, accountRef, objectRef); err != nil { + return err + } + + if err := db.DBImp.Get(ctx, objectRef, result); err != nil { + db.DBImp.Logger.Warn("Failed to get object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + return err + } + + db.DBImp.Logger.Debug("Successfully retrieved object", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", result.GetOrganizationRef()), + mzap.StorableRef(result), mzap.ObjRef("permission_ref", result.GetPermissionRef())) + return nil +} + +func (db *ProtectedDBImp[T]) Update(ctx context.Context, accountRef primitive.ObjectID, object T) error { + db.DBImp.Logger.Debug("Attempting to update object", mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(object)) + + if err := db.enforceObject(ctx, model.ActionUpdate, accountRef, *object.GetID()); err != nil { + return err + } + + if err := db.DBImp.Update(ctx, object); err != nil { + db.DBImp.Logger.Warn("Failed to update object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object)) + return err + } + + db.DBImp.Logger.Debug("Successfully updated object", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", object.GetOrganizationRef()), + mzap.StorableRef(object), mzap.ObjRef("permission_ref", object.GetPermissionRef())) + return nil +} + +func (db *ProtectedDBImp[T]) Delete(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { + db.DBImp.Logger.Debug("Attempting to delete object", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + + if err := db.enforceObject(ctx, model.ActionDelete, accountRef, objectRef); err != nil { + return err + } + + if err := db.DBImp.Delete(ctx, objectRef); err != nil { + db.DBImp.Logger.Warn("Failed to delete object", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return err + } + + db.DBImp.Logger.Debug("Successfully deleted object", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return nil +} + +func (db *ProtectedDBImp[T]) ListIDs( + ctx context.Context, + action model.Action, + accountRef primitive.ObjectID, + query builder.Query, +) ([]primitive.ObjectID, error) { + db.DBImp.Logger.Debug("Attempting to list object IDs", + mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery())) + + // 1. Fetch all candidate IDs from the underlying DB + allIDs, err := db.DBImp.ListPermissionBound(ctx, query) + if err != nil { + db.DBImp.Logger.Warn("Failed to list object IDs", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + zap.String("collection", string(db.Collection)), zap.String("action", string(action))) + return nil, err + } + if len(allIDs) == 0 { + db.DBImp.Logger.Debug("No objects found matching filter", mzap.ObjRef("account_ref", accountRef), + zap.String("collection", string(db.Collection)), zap.Any("filter", query.BuildQuery())) + return []primitive.ObjectID{}, merrors.NoData(fmt.Sprintf("no %s found", db.Collection)) + } + + // 2. Check read permission for each ID + var allowedIDs []primitive.ObjectID + for _, desc := range allIDs { + enforceErr := db.enforce(ctx, action, desc, accountRef, *desc.GetID()) + if enforceErr == nil { + allowedIDs = append(allowedIDs, *desc.GetID()) + } else if !errors.Is(err, merrors.ErrAccessDenied) { + // If the error is something other than AccessDenied, we want to fail + db.DBImp.Logger.Warn("Error while enforcing read permission", zap.Error(enforceErr), + mzap.ObjRef("permission_ref", desc.GetPermissionRef()), zap.String("action", string(action)), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()), + mzap.ObjRef("object_ref", *desc.GetID()), zap.String("collection", string(db.Collection)), + ) + return nil, enforceErr + } + // If AccessDenied, we simply skip that ID. + } + + db.DBImp.Logger.Debug("Successfully enforced read permission on IDs", zap.Int("fetched_count", len(allIDs)), + zap.Int("allowed_count", len(allowedIDs)), mzap.ObjRef("account_ref", accountRef), + zap.String("collection", string(db.Collection)), zap.String("action", string(action))) + + // 3. Return only the IDs that passed permission checks + return allowedIDs, nil +} + +func (db *ProtectedDBImp[T]) Unprotected() template.DB[T] { + return db.DBImp +} + +func (db *ProtectedDBImp[T]) DeleteCascadeAuth(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { + if err := db.enforceObject(ctx, model.ActionDelete, accountRef, objectRef); err != nil { + return err + } + if err := db.DBImp.DeleteCascade(ctx, objectRef); err != nil { + db.DBImp.Logger.Warn("Failed to delete dependent object", zap.Error(err)) + return err + } + return nil +} + +func CreateDBImp[T model.PermissionBoundStorable]( + ctx context.Context, + l mlogger.Logger, + pdb policy.DB, + enforcer Enforcer, + collection mservice.Type, + db *mongo.Database, +) (*ProtectedDBImp[T], error) { + logger := l.Named("protected") + var policy model.PolicyDescription + if err := pdb.GetBuiltInPolicy(ctx, collection, &policy); err != nil { + logger.Warn("Failed to fetch policy description", zap.Error(err), zap.String("resource_type", string(collection))) + return nil, err + } + p := &ProtectedDBImp[T]{ + DBImp: template.Create[T](logger, collection, db), + PermissionRef: policy.ID, + Collection: collection, + Enforcer: enforcer, + } + if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ + Keys: []ri.Key{{Field: storable.OrganizationRefField, Sort: ri.Asc}}, + }); err != nil { + logger.Warn("Failed to create index", zap.Error(err), zap.String("resource_type", string(collection))) + return nil, err + } + + return p, nil +} + +func (db *ProtectedDBImp[T]) Patch(ctx context.Context, accountRef, objectRef primitive.ObjectID, patch builder.Patch) error { + db.DBImp.Logger.Debug("Attempting to patch object", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + + if err := db.enforceObject(ctx, model.ActionUpdate, accountRef, objectRef); err != nil { + return err + } + + if err := db.DBImp.Repository.Patch(ctx, objectRef, patch); err != nil { + db.DBImp.Logger.Warn("Failed to patch object", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return err + } + + db.DBImp.Logger.Debug("Successfully patched object", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return nil +} + +func (db *ProtectedDBImp[T]) PatchMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, patch builder.Patch) (int, error) { + db.DBImp.Logger.Debug("Attempting to patch many objects", + mzap.ObjRef("account_ref", accountRef), zap.Any("filter", query.BuildQuery())) + + ids, err := db.ListIDs(ctx, model.ActionUpdate, accountRef, query) + if err != nil { + return 0, err + } + if len(ids) == 0 { + return 0, nil + } + + values := make([]any, len(ids)) + for i, id := range ids { + values[i] = id + } + idFilter := repository.Query().In(repository.IDField(), values...) + finalQuery := query.And(idFilter) + + modified, err := db.DBImp.Repository.PatchMany(ctx, finalQuery, patch) + if err != nil { + db.DBImp.Logger.Warn("Failed to patch many objects", zap.Error(err), + mzap.ObjRef("account_ref", accountRef)) + return 0, err + } + + db.DBImp.Logger.Debug("Successfully patched many objects", + mzap.ObjRef("account_ref", accountRef), zap.Int("modified_count", modified)) + return modified, nil +} diff --git a/api/pkg/auth/dbimpab.go b/api/pkg/auth/dbimpab.go new file mode 100644 index 0000000..44f6489 --- /dev/null +++ b/api/pkg/auth/dbimpab.go @@ -0,0 +1,420 @@ +package auth + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type AccountBoundDBImp[T model.AccountBoundStorable] struct { + Logger mlogger.Logger + DBImp *template.DBImp[T] + Enforcer Enforcer + PermissionRef primitive.ObjectID + Collection mservice.Type +} + +func (db *AccountBoundDBImp[T]) enforce(ctx context.Context, action model.Action, object model.AccountBoundStorable, accountRef primitive.ObjectID) error { + // FIRST: Check if the object's AccountRef equals the calling accountRef - if so, ALLOW + objectAccountRef := object.GetAccountRef() + if objectAccountRef != nil && *objectAccountRef == accountRef { + db.Logger.Debug("Access granted - object belongs to calling account", + mzap.ObjRef("object_account_ref", *objectAccountRef), + mzap.ObjRef("calling_account_ref", accountRef), + zap.String("action", string(action))) + return nil + } + + // SECOND: If not owned by calling account, check organization-level permissions + organizationRef := object.GetOrganizationRef() + res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, organizationRef, action) + if err != nil { + db.Logger.Warn("Failed to enforce permission", + zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + zap.String("action", string(action))) + return err + } + if !res { + db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + zap.String("action", string(action))) + return merrors.AccessDenied(db.Collection, string(action), primitive.NilObjectID) + } + return nil +} + +func (db *AccountBoundDBImp[T]) enforceInterface(ctx context.Context, action model.Action, object model.AccountBoundStorable, accountRef primitive.ObjectID) error { + // FIRST: Check if the object's AccountRef equals the calling accountRef - if so, ALLOW + objectAccountRef := object.GetAccountRef() + if objectAccountRef != nil && *objectAccountRef == accountRef { + db.Logger.Debug("Access granted - object belongs to calling account", + mzap.ObjRef("object_account_ref", *objectAccountRef), + mzap.ObjRef("calling_account_ref", accountRef), + zap.String("action", string(action))) + return nil + } + + // SECOND: If not owned by calling account, check organization-level permissions + organizationRef := object.GetOrganizationRef() + res, err := db.Enforcer.Enforce(ctx, db.PermissionRef, accountRef, organizationRef, organizationRef, action) + if err != nil { + db.Logger.Warn("Failed to enforce permission", + zap.Error(err), mzap.ObjRef("permission_ref", db.PermissionRef), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + zap.String("action", string(action))) + return err + } + if !res { + db.Logger.Debug("Access denied", mzap.ObjRef("permission_ref", db.PermissionRef), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef), + zap.String("action", string(action))) + return merrors.AccessDenied(db.Collection, string(action), primitive.NilObjectID) + } + return nil +} + +func (db *AccountBoundDBImp[T]) Create(ctx context.Context, accountRef primitive.ObjectID, object T) error { + orgRef := object.GetOrganizationRef() + db.Logger.Debug("Attempting to create object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) + + // Check organization update permission for create operations + if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil { + return err + } + + if err := db.DBImp.Create(ctx, object); err != nil { + db.Logger.Warn("Failed to create object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) + return err + } + + db.Logger.Debug("Successfully created object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", orgRef), zap.String("collection", string(db.Collection))) + return nil +} + +func (db *AccountBoundDBImp[T]) Get(ctx context.Context, accountRef, objectRef primitive.ObjectID, result T) error { + db.Logger.Debug("Attempting to get object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + + // First get the object to check its organization + if err := db.DBImp.Get(ctx, objectRef, result); err != nil { + db.Logger.Warn("Failed to get object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + return err + } + + // Check organization read permission + if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil { + return err + } + + db.Logger.Debug("Successfully retrieved object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", result.GetOrganizationRef()), zap.String("collection", string(db.Collection))) + return nil +} + +func (db *AccountBoundDBImp[T]) Update(ctx context.Context, accountRef primitive.ObjectID, object T) error { + db.Logger.Debug("Attempting to update object", mzap.ObjRef("account_ref", accountRef), mzap.StorableRef(object)) + + // Check organization update permission + if err := db.enforce(ctx, model.ActionUpdate, object, accountRef); err != nil { + return err + } + + if err := db.DBImp.Update(ctx, object); err != nil { + db.Logger.Warn("Failed to update object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object)) + return err + } + + db.Logger.Debug("Successfully updated object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", object.GetOrganizationRef()), mzap.StorableRef(object)) + return nil +} + +func (db *AccountBoundDBImp[T]) Patch(ctx context.Context, accountRef, objectRef primitive.ObjectID, patch builder.Patch) error { + db.Logger.Debug("Attempting to patch object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + + // First get the object to check its organization + objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef)) + if err != nil { + db.Logger.Warn("Failed to get object for permission check when deleting", zap.Error(err), mzap.ObjRef("object_ref", objectRef)) + return err + } + if len(objs) == 0 { + db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.ObjRef("account_ref", accountRef)) + return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef) + } + + // Check organization update permission + if err := db.enforce(ctx, model.ActionUpdate, objs[0], accountRef); err != nil { + return err + } + + if err := db.DBImp.Patch(ctx, objectRef, patch); err != nil { + db.Logger.Warn("Failed to patch object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + return err + } + + db.Logger.Debug("Successfully patched object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return nil +} + +func (db *AccountBoundDBImp[T]) Delete(ctx context.Context, accountRef, objectRef primitive.ObjectID) error { + db.Logger.Debug("Attempting to delete object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + + // First get the object to check its organization + objs, err := db.DBImp.Repository.ListAccountBound(ctx, repository.IDFilter(objectRef)) + if err != nil { + db.Logger.Warn("Failed to get object for permission check when deleting", zap.Error(err), mzap.ObjRef("object_ref", objectRef)) + return err + } + if len(objs) == 0 { + db.Logger.Debug("Permission denied for deletion", mzap.ObjRef("object_ref", objectRef), mzap.ObjRef("account_ref", accountRef)) + return merrors.AccessDenied(db.Collection, string(model.ActionDelete), objectRef) + } + // Check organization update permission for delete operations + if err := db.enforce(ctx, model.ActionUpdate, objs[0], accountRef); err != nil { + return err + } + + if err := db.DBImp.Delete(ctx, objectRef); err != nil { + db.Logger.Warn("Failed to delete object", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("collection", string(db.Collection))) + return err + } + + db.Logger.Debug("Successfully deleted object", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return nil +} + +func (db *AccountBoundDBImp[T]) DeleteMany(ctx context.Context, accountRef primitive.ObjectID, query builder.Query) error { + db.Logger.Debug("Attempting to delete many objects", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) + + // Get all candidate objects for batch permission checking + allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query) + if err != nil { + db.Logger.Warn("Failed to list objects for delete many", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + // Use batch enforcement for efficiency + allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionUpdate) + if err != nil { + db.Logger.Warn("Failed to enforce batch permissions for delete many", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + // Build query for objects that passed permission check + var allowedIDs []primitive.ObjectID + for _, obj := range allObjects { + if allowedResults[*obj.GetID()] { + allowedIDs = append(allowedIDs, *obj.GetID()) + } + } + + if len(allowedIDs) == 0 { + db.Logger.Debug("No objects allowed for deletion", mzap.ObjRef("account_ref", accountRef)) + return nil + } + + // Delete only the allowed objects + allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs)) + if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil { + db.Logger.Warn("Failed to delete many objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + db.Logger.Debug("Successfully deleted many objects", mzap.ObjRef("account_ref", accountRef), zap.Int("count", len(allowedIDs))) + return nil +} + +func (db *AccountBoundDBImp[T]) FindOne(ctx context.Context, accountRef primitive.ObjectID, query builder.Query, result T) error { + db.Logger.Debug("Attempting to find one object", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) + + // For FindOne, we need to check read permission after finding the object + if err := db.DBImp.FindOne(ctx, query, result); err != nil { + db.Logger.Warn("Failed to find one object", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + // Check organization read permission for the found object + if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil { + return err + } + + db.Logger.Debug("Successfully found one object", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", result.GetOrganizationRef())) + return nil +} + +func (db *AccountBoundDBImp[T]) ListIDs(ctx context.Context, accountRef primitive.ObjectID, query builder.Query) ([]primitive.ObjectID, error) { + db.Logger.Debug("Attempting to list object IDs", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) + + // Get all candidate objects for batch permission checking + allObjects, err := db.DBImp.Repository.ListPermissionBound(ctx, query) + if err != nil { + db.Logger.Warn("Failed to list objects for ID filtering", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return nil, err + } + + // Use batch enforcement for efficiency + allowedResults, err := db.Enforcer.EnforceBatch(ctx, allObjects, accountRef, model.ActionRead) + if err != nil { + db.Logger.Warn("Failed to enforce batch permissions for ID listing", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return nil, err + } + + // Filter to only allowed object IDs + var allowedIDs []primitive.ObjectID + for _, obj := range allObjects { + if allowedResults[*obj.GetID()] { + allowedIDs = append(allowedIDs, *obj.GetID()) + } + } + + db.Logger.Debug("Successfully filtered object IDs", zap.Int("total_count", len(allObjects)), + zap.Int("allowed_count", len(allowedIDs)), mzap.ObjRef("account_ref", accountRef)) + return allowedIDs, nil +} + +func (db *AccountBoundDBImp[T]) ListAccountBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID, query builder.Query) ([]model.AccountBoundStorable, error) { + db.Logger.Debug("Attempting to list account bound objects", mzap.ObjRef("account_ref", accountRef), zap.String("collection", string(db.Collection))) + + // Build query to find objects where accountRef matches OR is null/absent + accountQuery := repository.WithOrg(accountRef, organizationRef) + + // Combine with the provided query + finalQuery := query.And(accountQuery) + + // Get all candidate objects + allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, finalQuery) + if err != nil { + db.Logger.Warn("Failed to list account bound objects", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return nil, err + } + + // Filter objects based on read permissions (AccountBoundStorable doesn't have permission info, so we check organization level) + var allowedObjects []model.AccountBoundStorable + for _, obj := range allObjects { + if err := db.enforceInterface(ctx, model.ActionRead, obj, accountRef); err == nil { + allowedObjects = append(allowedObjects, obj) + } else if !errors.Is(err, merrors.ErrAccessDenied) { + // If the error is something other than AccessDenied, we want to fail + db.Logger.Warn("Error while enforcing read permission", zap.Error(err), mzap.ObjRef("object_ref", *obj.GetID())) + return nil, err + } + // If AccessDenied, we simply skip that object + } + + db.Logger.Debug("Successfully filtered account bound objects", zap.Int("total_count", len(allObjects)), + zap.Int("allowed_count", len(allowedObjects)), mzap.ObjRef("account_ref", accountRef)) + return allowedObjects, nil +} + +func (db *AccountBoundDBImp[T]) GetByAccountRef(ctx context.Context, accountRef primitive.ObjectID, result T) error { + db.Logger.Debug("Attempting to get object by account ref", mzap.ObjRef("account_ref", accountRef)) + + // Build query to find objects where accountRef matches OR is null/absent + query := repository.WithoutOrg(accountRef) + + if err := db.DBImp.FindOne(ctx, query, result); err != nil { + db.Logger.Warn("Failed to get object by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + // Check organization read permission for the found object + if err := db.enforce(ctx, model.ActionRead, result, accountRef); err != nil { + return err + } + + db.Logger.Debug("Successfully retrieved object by account ref", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", result.GetOrganizationRef())) + return nil +} + +func (db *AccountBoundDBImp[T]) DeleteByAccountRef(ctx context.Context, accountRef primitive.ObjectID) error { + db.Logger.Debug("Attempting to delete objects by account ref", mzap.ObjRef("account_ref", accountRef)) + + // Build query to find objects where accountRef matches OR is null/absent + query := repository.WithoutOrg(accountRef) + + // Get all candidate objects for individual permission checking + allObjects, err := db.DBImp.Repository.ListAccountBound(ctx, query) + if err != nil { + db.Logger.Warn("Failed to list objects for delete by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + // Check permissions for each object individually (AccountBoundStorable doesn't have permission info) + var allowedIDs []primitive.ObjectID + for _, obj := range allObjects { + if err := db.enforceInterface(ctx, model.ActionUpdate, obj, accountRef); err == nil { + allowedIDs = append(allowedIDs, *obj.GetID()) + } else if !errors.Is(err, merrors.ErrAccessDenied) { + // If the error is something other than AccessDenied, we want to fail + db.Logger.Warn("Error while enforcing update permission", zap.Error(err), mzap.ObjRef("object_ref", *obj.GetID())) + return err + } + // If AccessDenied, we simply skip that object + } + + if len(allowedIDs) == 0 { + db.Logger.Debug("No objects allowed for deletion by account ref", mzap.ObjRef("account_ref", accountRef)) + return nil + } + + // Delete only the allowed objects + allowedQuery := query.And(repository.Query().In(repository.IDField(), allowedIDs)) + if err := db.DBImp.DeleteMany(ctx, allowedQuery); err != nil { + db.Logger.Warn("Failed to delete objects by account ref", zap.Error(err), mzap.ObjRef("account_ref", accountRef)) + return err + } + + db.Logger.Debug("Successfully deleted objects by account ref", mzap.ObjRef("account_ref", accountRef), zap.Int("count", len(allowedIDs))) + return nil +} + +func (db *AccountBoundDBImp[T]) DeleteCascade(ctx context.Context, objectRef primitive.ObjectID) error { + return db.DBImp.DeleteCascade(ctx, objectRef) +} + +// CreateAccountBoundImp creates a concrete AccountBoundDBImp instance for internal use +func CreateAccountBoundImp[T model.AccountBoundStorable]( + ctx context.Context, + logger mlogger.Logger, + pdb policy.DB, + enforcer Enforcer, + collection mservice.Type, + db *mongo.Database, +) (*AccountBoundDBImp[T], error) { + logger = logger.Named("account_bound") + var policy model.PolicyDescription + if err := pdb.GetBuiltInPolicy(ctx, mservice.Organizations, &policy); err != nil { + logger.Warn("Failed to fetch organization policy description", zap.Error(err)) + return nil, err + } + res := &AccountBoundDBImp[T]{ + Logger: logger, + DBImp: template.Create[T](logger, collection, db), + Enforcer: enforcer, + PermissionRef: policy.ID, + Collection: collection, + } + return res, nil +} diff --git a/api/pkg/auth/dbimpab_test.go b/api/pkg/auth/dbimpab_test.go new file mode 100644 index 0000000..1ccdb9c --- /dev/null +++ b/api/pkg/auth/dbimpab_test.go @@ -0,0 +1,81 @@ +package auth + +import ( + "errors" + "testing" + + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/stretchr/testify/assert" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// TestAccountBoundDBImp_Enforce tests the enforce method +func TestAccountBoundDBImp_Enforce(t *testing.T) { + logger := mlogger.Logger(zap.NewNop()) + db := &AccountBoundDBImp[model.AccountBoundStorable]{ + Logger: logger, + PermissionRef: primitive.NewObjectID(), + Collection: "test_collection", + } + + t.Run("EnforceMethodExists", func(t *testing.T) { + // Test that the enforce method exists and can be called + // This is a basic test to ensure the method signature is correct + assert.NotNil(t, db.enforce) + }) + + t.Run("PermissionRefSet", func(t *testing.T) { + // Test that PermissionRef is properly set + assert.NotEqual(t, primitive.NilObjectID, db.PermissionRef) + }) + + t.Run("CollectionSet", func(t *testing.T) { + // Test that Collection is properly set + assert.Equal(t, "test_collection", string(db.Collection)) + }) +} + +// TestAccountBoundDBImp_InterfaceCompliance tests that the struct implements required interfaces +func TestAccountBoundDBImp_InterfaceCompliance(t *testing.T) { + logger := mlogger.Logger(zap.NewNop()) + db := &AccountBoundDBImp[model.AccountBoundStorable]{ + Logger: logger, + PermissionRef: primitive.NewObjectID(), + Collection: "test_collection", + } + + t.Run("StructInitialization", func(t *testing.T) { + // Test that the struct can be initialized + assert.NotNil(t, db) + assert.NotNil(t, db.Logger) + assert.NotEqual(t, primitive.NilObjectID, db.PermissionRef) + assert.NotEmpty(t, db.Collection) + }) + + t.Run("LoggerInitialization", func(t *testing.T) { + // Test that logger is properly initialized + assert.NotNil(t, db.Logger) + }) +} + +// TestAccountBoundDBImp_ErrorHandling tests error handling patterns +func TestAccountBoundDBImp_ErrorHandling(t *testing.T) { + t.Run("AccessDeniedError", func(t *testing.T) { + // Test that AccessDenied error is properly created + err := merrors.AccessDenied("test_collection", "read", primitive.NilObjectID) + assert.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrAccessDenied)) + }) + + t.Run("ErrorTypeChecking", func(t *testing.T) { + // Test error type checking + accessDeniedErr := merrors.AccessDenied("test", "read", primitive.NilObjectID) + otherErr := errors.New("other error") + + assert.True(t, errors.Is(accessDeniedErr, merrors.ErrAccessDenied)) + assert.False(t, errors.Is(otherErr, merrors.ErrAccessDenied)) + }) +} diff --git a/api/pkg/auth/enforcer.go b/api/pkg/auth/enforcer.go new file mode 100644 index 0000000..eb0c0b3 --- /dev/null +++ b/api/pkg/auth/enforcer.go @@ -0,0 +1,32 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +type Enforcer interface { + // Enforce checks if accountRef can do `action` on objectRef in an org (domainRef). + Enforce( + ctx context.Context, + permissionRef, accountRef, orgRef, objectRef primitive.ObjectID, + action model.Action, + ) (bool, error) + + // Enforce batch of objects + EnforceBatch( + ctx context.Context, + objectRefs []model.PermissionBoundStorable, + accountRef primitive.ObjectID, + action model.Action, + ) (map[primitive.ObjectID]bool, error) + + // GetRoles returns the user's roles in a given org domain, plus any partial scopes if relevant. + GetRoles(ctx context.Context, accountRef, orgRef primitive.ObjectID) ([]model.Role, error) + + // GetPermissions returns all effective permissions (with effect, object scoping) for a user in org domain. + // Merges from all roles the user holds, plus any denies/exceptions. + GetPermissions(ctx context.Context, accountRef, orgRef primitive.ObjectID) ([]model.Role, []model.Permission, error) +} diff --git a/api/pkg/auth/factory.go b/api/pkg/auth/factory.go new file mode 100644 index 0000000..b4b6a55 --- /dev/null +++ b/api/pkg/auth/factory.go @@ -0,0 +1,52 @@ +package auth + +import ( + "github.com/tech/sendico/pkg/auth/internal/casbin" + "github.com/tech/sendico/pkg/auth/internal/native" + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/role" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +func CreateAuth( + logger mlogger.Logger, + client *mongo.Client, + db *mongo.Database, + pdb policy.DB, + rdb role.DB, + config *Config, +) (Enforcer, Manager, error) { + lg := logger.Named("auth") + lg.Debug("Creating enforcer...", zap.String("driver", string(config.Driver))) + l := lg.Named(string(config.Driver)) + if config.Driver == Casbin { + enforcer, err := casbin.NewEnforcer(l, client, config.Settings) + if err != nil { + lg.Warn("Failed to create enforcer", zap.Error(err)) + return nil, nil, err + } + manager, err := casbin.NewManager(l, pdb, rdb, enforcer, config.Settings) + if err != nil { + lg.Warn("Failed to create managment interface", zap.Error(err)) + return nil, nil, err + } + return enforcer, manager, nil + } + if config.Driver == Native { + enforcer, err := native.NewEnforcer(l, db) + if err != nil { + lg.Warn("Failed to create enforcer", zap.Error(err)) + return nil, nil, err + } + manager, err := native.NewManager(l, pdb, rdb, enforcer) + if err != nil { + lg.Warn("Failed to create managment interface", zap.Error(err)) + return nil, nil, err + } + return enforcer, manager, nil + } + return nil, nil, merrors.InvalidArgument("Unknown enforcer type: " + string(config.Driver)) +} diff --git a/api/pkg/auth/helper.go b/api/pkg/auth/helper.go new file mode 100644 index 0000000..4d448fc --- /dev/null +++ b/api/pkg/auth/helper.go @@ -0,0 +1,61 @@ +package auth + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +func enforceObject[T model.PermissionBoundStorable](ctx context.Context, db *template.DBImp[T], enforcer Enforcer, action model.Action, accountRef primitive.ObjectID, query builder.Query) error { + l, err := db.ListPermissionBound(ctx, query) + if err != nil { + db.Logger.Warn("Error occured while checking access rights", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), zap.String("action", string(action))) + return err + } + if len(l) == 0 { + db.Logger.Debug("Access denied", mzap.ObjRef("account_ref", accountRef), zap.String("action", string(action))) + return merrors.AccessDenied(db.Repository.Collection(), string(action), primitive.NilObjectID) + } + for _, item := range l { + db.Logger.Debug("Object found", mzap.ObjRef("object_ref", *item.GetID()), + mzap.ObjRef("organization_ref", item.GetOrganizationRef()), + mzap.ObjRef("permission_ref", item.GetPermissionRef()), + zap.String("collection", item.Collection())) + } + res, err := enforcer.EnforceBatch(ctx, l, accountRef, action) + if err != nil { + db.Logger.Warn("Failed to enforce permission", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), zap.String("action", string(action))) + } + for objectRef, hasPermission := range res { + if !hasPermission { + db.Logger.Info("Permission denied for object during reordering", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionUpdate))) + return merrors.AccessDenied(db.Repository.Collection(), string(action), objectRef) + } + } + return nil +} + +func enforceObjectByRef[T model.PermissionBoundStorable](ctx context.Context, db *template.DBImp[T], enforcer Enforcer, action model.Action, accountRef, objectRef primitive.ObjectID) error { + err := enforceObject(ctx, db, enforcer, action, accountRef, repository.IDFilter(objectRef)) + if err != nil { + if errors.Is(err, merrors.ErrAccessDenied) { + db.Logger.Debug("Access denied", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return merrors.AccessDenied(db.Repository.Collection(), string(action), objectRef) + } else { + db.Logger.Warn("Error occurred while checking permissions", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + } + } + return err +} diff --git a/api/pkg/auth/indexable.go b/api/pkg/auth/indexable.go new file mode 100644 index 0000000..a0e620b --- /dev/null +++ b/api/pkg/auth/indexable.go @@ -0,0 +1,29 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// IndexableDB implements reordering with permission checking +type IndexableDB[T storable.Storable] interface { + // Reorder implements reordering with permission checking using EnforceBatch + Reorder(ctx context.Context, accountRef, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error +} + +// NewIndexableDB creates a new auth.IndexableDB instance +func NewIndexableDB[T storable.Storable]( + repo repository.Repository, + logger mlogger.Logger, + enforcer Enforcer, + createEmpty func() T, + getIndexable func(T) *model.Indexable, +) IndexableDB[T] { + return newIndexableDBImp(repo, logger, enforcer, createEmpty, getIndexable) +} diff --git a/api/pkg/auth/indexableimp.go b/api/pkg/auth/indexableimp.go new file mode 100644 index 0000000..ec03cb3 --- /dev/null +++ b/api/pkg/auth/indexableimp.go @@ -0,0 +1,182 @@ +package auth + +import ( + "context" + + "github.com/tech/sendico/pkg/db/repository" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// IndexableDB implements reordering with permission checking +type indexableDBImp[T storable.Storable] struct { + repo repository.Repository + logger mlogger.Logger + enforcer Enforcer + createEmpty func() T + getIndexable func(T) *model.Indexable +} + +// NewIndexableDB creates a new auth.IndexableDB instance +func newIndexableDBImp[T storable.Storable]( + repo repository.Repository, + logger mlogger.Logger, + enforcer Enforcer, + createEmpty func() T, + getIndexable func(T) *model.Indexable, +) IndexableDB[T] { + return &indexableDBImp[T]{ + repo: repo, + logger: logger.Named("indexable"), + enforcer: enforcer, + createEmpty: createEmpty, + getIndexable: getIndexable, + } +} + +// Reorder implements reordering with permission checking using EnforceBatch +func (db *indexableDBImp[T]) Reorder(ctx context.Context, accountRef, objectRef primitive.ObjectID, newIndex int, filter builder.Query) error { + // Get current object to find its index + obj := db.createEmpty() + if err := db.repo.Get(ctx, objectRef, obj); err != nil { + db.logger.Warn("Failed to get object for reordering", zap.Error(err), zap.Int("new_index", newIndex), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return err + } + + // Extract index from the object + indexable := db.getIndexable(obj) + currentIndex := indexable.Index + if currentIndex == newIndex { + db.logger.Debug("No reordering needed - same index", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) + return nil // No change needed + } + + // Determine which objects will be affected by the reordering + var affectedObjects []model.PermissionBoundStorable + + if currentIndex < newIndex { + // Moving down: items between currentIndex+1 and newIndex will be shifted up by -1 + reorderFilter := filter. + And(repository.IndexOpFilter(currentIndex+1, builder.Gte)). + And(repository.IndexOpFilter(newIndex, builder.Lte)) + + // Get all affected objects using ListPermissionBound + objects, err := db.repo.ListPermissionBound(ctx, reorderFilter) + if err != nil { + db.logger.Warn("Failed to get affected objects for reordering (moving down)", + zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), + zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) + return err + } + affectedObjects = append(affectedObjects, objects...) + db.logger.Debug("Found affected objects for moving down", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects))) + } else { + // Moving up: items between newIndex and currentIndex-1 will be shifted down by +1 + reorderFilter := filter. + And(repository.IndexOpFilter(newIndex, builder.Gte)). + And(repository.IndexOpFilter(currentIndex-1, builder.Lte)) + + // Get all affected objects using ListPermissionBound + objects, err := db.repo.ListPermissionBound(ctx, reorderFilter) + if err != nil { + db.logger.Warn("Failed to get affected objects for reordering (moving up)", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), + zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) + return err + } + affectedObjects = append(affectedObjects, objects...) + db.logger.Debug("Found affected objects for moving up", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(objects))) + } + + // Add the target object to the list of objects that need permission checking + targetObjects, err := db.repo.ListPermissionBound(ctx, repository.IDFilter(objectRef)) + if err != nil { + db.logger.Warn("Failed to get target object for permission checking", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef)) + return err + } + if len(targetObjects) > 0 { + affectedObjects = append(affectedObjects, targetObjects[0]) + } + + // Check permissions for all affected objects using EnforceBatch + db.logger.Debug("Checking permissions for reordering", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects)), + zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) + + permissions, err := db.enforcer.EnforceBatch(ctx, affectedObjects, accountRef, model.ActionUpdate) + if err != nil { + db.logger.Warn("Failed to check permissions for reordering", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("affected_count", len(affectedObjects))) + return merrors.Internal("failed to check permissions for reordering") + } + + // Verify all objects have update permission + for resObjectRef, hasPermission := range permissions { + if !hasPermission { + db.logger.Info("Permission denied for object during reordering", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(model.ActionUpdate))) + return merrors.AccessDenied(db.repo.Collection(), string(model.ActionUpdate), resObjectRef) + } + } + + db.logger.Debug("All permissions granted, proceeding with reordering", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("permission_count", len(permissions))) + + // All permissions checked, proceed with reordering + if currentIndex < newIndex { + // Moving down: shift items between currentIndex+1 and newIndex up by -1 + patch := repository.Patch().Inc(repository.IndexField(), -1) + reorderFilter := filter. + And(repository.IndexOpFilter(currentIndex+1, builder.Gte)). + And(repository.IndexOpFilter(newIndex, builder.Lte)) + + updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch) + if err != nil { + db.logger.Warn("Failed to shift objects during reordering (moving down)", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), + zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("updated_count", updatedCount)) + return err + } + db.logger.Debug("Successfully shifted objects (moving down)", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount)) + } else { + // Moving up: shift items between newIndex and currentIndex-1 down by +1 + patch := repository.Patch().Inc(repository.IndexField(), 1) + reorderFilter := filter. + And(repository.IndexOpFilter(newIndex, builder.Gte)). + And(repository.IndexOpFilter(currentIndex-1, builder.Lte)) + + updatedCount, err := db.repo.PatchMany(ctx, reorderFilter, patch) + if err != nil { + db.logger.Warn("Failed to shift objects during reordering (moving up)", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), + zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex), zap.Int("updated_count", updatedCount)) + return err + } + db.logger.Debug("Successfully shifted objects (moving up)", mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("updated_count", updatedCount)) + } + + // Update the target object to new index + if err := db.repo.Patch(ctx, objectRef, repository.Patch().Set(repository.IndexField(), newIndex)); err != nil { + db.logger.Warn("Failed to update target object index", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("object_ref", objectRef), zap.Int("current_index", currentIndex), zap.Int("new_index", newIndex)) + return err + } + + db.logger.Debug("Successfully reordered object with permission checking", + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("object_ref", objectRef), zap.Int("old_index", currentIndex), + zap.Int("new_index", newIndex), zap.Int("affected_count", len(affectedObjects))) + return nil +} diff --git a/api/pkg/auth/internal/casbin/action.go b/api/pkg/auth/internal/casbin/action.go new file mode 100644 index 0000000..8e25dad --- /dev/null +++ b/api/pkg/auth/internal/casbin/action.go @@ -0,0 +1,23 @@ +package casbin + +import ( + "fmt" + + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" +) + +func stringToAction(actionStr string) (model.Action, error) { + switch actionStr { + case string(model.ActionCreate): + return model.ActionCreate, nil + case string(model.ActionRead): + return model.ActionRead, nil + case string(model.ActionUpdate): + return model.ActionUpdate, nil + case string(model.ActionDelete): + return model.ActionDelete, nil + default: + return "", merrors.InvalidArgument(fmt.Sprintf("invalid action: %s", actionStr)) + } +} diff --git a/api/pkg/auth/internal/casbin/config/config.go b/api/pkg/auth/internal/casbin/config/config.go new file mode 100644 index 0000000..17d1f47 --- /dev/null +++ b/api/pkg/auth/internal/casbin/config/config.go @@ -0,0 +1,126 @@ +package casbin + +import ( + "os" + "time" + + mongodbadapter "github.com/casbin/mongodb-adapter/v3" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" +) + +type AdapterConfig struct { + DatabaseName *string `mapstructure:"database_name"` + DatabaseNameEnv *string `mapstructure:"database_name_env"` + CollectionName *string `mapstructure:"collection_name"` + CollectionNameEnv *string `mapstructure:"collection_name_env"` + TimeoutSeconds *int `mapstructure:"timeout_seconds"` + TimeoutSecondsEnv *string `mapstructure:"timeout_seconds_env"` + IsFiltered *bool `mapstructure:"is_filtered"` + IsFilteredEnv *string `mapstructure:"is_filtered_env"` +} + +type Config struct { + ModelPath *string `mapstructure:"model_path"` + ModelPathEnv *string `mapstructure:"model_path_env"` + Adapter *AdapterConfig `mapstructure:"adapter"` +} + +type EnforcerConfig struct { + ModelPath string + Adapter *mongodbadapter.AdapterConfig +} + +func getEnvValue(logger mlogger.Logger, varName, envVarName string, value, envValue *string) string { + if value != nil && envValue != nil { + logger.Warn("Both variable and environment variable are set, using environment variable value", + zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.String("value", *value), zap.String("env_value", os.Getenv(*envValue))) + } + + if envValue != nil { + return os.Getenv(*envValue) + } + + if value != nil { + return *value + } + + return "" +} + +func getEnvIntValue(logger mlogger.Logger, varName, envVarName string, value *int, envValue *string) int { + if value != nil && envValue != nil { + logger.Warn("Both variable and environment variable are set, using environment variable value", + zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.Int("value", *value), zap.String("env_value", os.Getenv(*envValue))) + } + + if envValue != nil { + envStr := os.Getenv(*envValue) + if envStr != "" { + if parsed, err := time.ParseDuration(envStr + "s"); err == nil { + return int(parsed.Seconds()) + } + logger.Warn("Invalid environment variable value for timeout", zap.String("environment_variable", envVarName), zap.String("value", envStr)) + } + } + + if value != nil { + return *value + } + + return 30 // Default timeout in seconds +} + +func getEnvBoolValue(logger mlogger.Logger, varName, envVarName string, value *bool, envValue *string) bool { + if value != nil && envValue != nil { + logger.Warn("Both variable and environment variable are set, using environment variable value", + zap.String("variable", varName), zap.String("environment_variable", envVarName), zap.Bool("value", *value), zap.String("env_value", os.Getenv(*envValue))) + } + + if envValue != nil { + envStr := os.Getenv(*envValue) + if envStr == "true" || envStr == "1" { + return true + } else if envStr == "false" || envStr == "0" { + return false + } + logger.Warn("Invalid environment variable value for boolean", zap.String("environment_variable", envVarName), zap.String("value", envStr)) + } + + if value != nil { + return *value + } + + return false // Default for boolean +} + +func PrepareConfig(logger mlogger.Logger, config *Config) (*EnforcerConfig, error) { + if config == nil { + return nil, merrors.Internal("No configuration provided") + } + + adapter := &mongodbadapter.AdapterConfig{ + DatabaseName: getEnvValue(logger, "database_name", "database_name_env", config.Adapter.DatabaseName, config.Adapter.DatabaseNameEnv), + CollectionName: getEnvValue(logger, "collection_name", "collection_name_env", config.Adapter.CollectionName, config.Adapter.CollectionNameEnv), + Timeout: time.Duration(getEnvIntValue(logger, "timeout_seconds", "timeout_seconds_env", config.Adapter.TimeoutSeconds, config.Adapter.TimeoutSecondsEnv)) * time.Second, + IsFiltered: getEnvBoolValue(logger, "is_filtered", "is_filtered_env", config.Adapter.IsFiltered, config.Adapter.IsFilteredEnv), + } + + if len(adapter.DatabaseName) == 0 { + logger.Error("Database name is not set") + return nil, merrors.InvalidArgument("database name must be provided") + } + + path := getEnvValue(logger, "model_path", "model_path_env", config.ModelPath, config.ModelPathEnv) + + logger.Info("Configuration prepared", + zap.String("model_path", path), + zap.String("database_name", adapter.DatabaseName), + zap.String("collection_name", adapter.CollectionName), + zap.Duration("timeout", adapter.Timeout), + zap.Bool("is_filtered", adapter.IsFiltered), + ) + + return &EnforcerConfig{ModelPath: path, Adapter: adapter}, nil +} diff --git a/api/pkg/auth/internal/casbin/enforcer.go b/api/pkg/auth/internal/casbin/enforcer.go new file mode 100644 index 0000000..ba2fe6c --- /dev/null +++ b/api/pkg/auth/internal/casbin/enforcer.go @@ -0,0 +1,206 @@ +// casbin_enforcer.go +package casbin + +import ( + "context" + + "github.com/casbin/casbin/v2" + "github.com/tech/sendico/pkg/auth/anyobject" + cc "github.com/tech/sendico/pkg/auth/internal/casbin/config" + "github.com/tech/sendico/pkg/auth/internal/casbin/serialization" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "github.com/mitchellh/mapstructure" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +// CasbinEnforcer implements the Enforcer interface using Casbin. +type CasbinEnforcer struct { + logger mlogger.Logger + enforcer *casbin.Enforcer + roleSerializer serialization.Role + permissionSerializer serialization.Policy +} + +// NewCasbinEnforcer initializes a new CasbinEnforcer with a MongoDB adapter, logger, and PolicySerializer. +// The 'domain' parameter is no longer stored internally, as the interface requires passing a domainRef per method call. +func NewEnforcer( + logger mlogger.Logger, + client *mongo.Client, + settings model.SettingsT, +) (*CasbinEnforcer, error) { + var config cc.Config + if err := mapstructure.Decode(settings, &config); err != nil { + logger.Warn("Failed to decode Casbin configuration", zap.Error(err), zap.Any("settings", settings)) + return nil, merrors.Internal("failed to decode Casbin configuration") + } + + // Create a Casbin adapter + enforcer from your config and client. + l := logger.Named("enforcer") + e, err := createAdapter(l, &config, client) + if err != nil { + logger.Warn("Failed to create Casbin enforcer", zap.Error(err)) + return nil, merrors.Internal("failed to create Casbin enforcer") + } + + logger.Info("Casbin enforcer created") + return &CasbinEnforcer{ + logger: l, + enforcer: e, + permissionSerializer: serialization.NewPolicySerializer(), + roleSerializer: serialization.NewRoleSerializer(), + }, nil +} + +// Enforce checks if a user has the specified action permission on an object within a domain. +func (c *CasbinEnforcer) Enforce( + _ context.Context, + permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID, + action model.Action, +) (bool, error) { + // Convert ObjectIDs to strings for Casbin + account := accountRef.Hex() + organization := organizationRef.Hex() + permission := permissionRef.Hex() + object := anyobject.ID + if objectRef != primitive.NilObjectID { + object = objectRef.Hex() + } + act := string(action) + + c.logger.Debug("Enforcing policy", + zap.String("account", account), zap.String("organization", organization), + zap.String("permission", permission), zap.String("object", object), + zap.String("action", act)) + + // Perform the enforcement + result, err := c.enforcer.Enforce(account, organization, permission, object, act) + if err != nil { + c.logger.Warn("Failed to enforce policy", zap.Error(err), + zap.String("account", account), zap.String("organization", organization), + zap.String("permission", permission), zap.String("object", object), + zap.String("action", act)) + return false, err + } + + c.logger.Debug("Policy enforcement result", zap.Bool("result", result)) + return result, nil +} + +// EnforceBatch checks a user’s permission for multiple objects at once. +// It returns a map from objectRef -> boolean indicating whether access is granted. +func (c *CasbinEnforcer) EnforceBatch( + ctx context.Context, + objectRefs []model.PermissionBoundStorable, + accountRef primitive.ObjectID, + action model.Action, +) (map[primitive.ObjectID]bool, error) { + results := make(map[primitive.ObjectID]bool, len(objectRefs)) + for _, desc := range objectRefs { + ok, err := c.Enforce(ctx, desc.GetPermissionRef(), accountRef, desc.GetOrganizationRef(), *desc.GetID(), action) + if err != nil { + c.logger.Warn("Failed to enforce", zap.Error(err), mzap.ObjRef("permission_ref", desc.GetPermissionRef()), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", desc.GetOrganizationRef()), + mzap.ObjRef("object_ref", *desc.GetID()), zap.String("action", string(action))) + return nil, err + } + results[*desc.GetID()] = ok + } + + return results, nil +} + +// GetRoles retrieves all roles assigned to the user within the domain. +func (c *CasbinEnforcer) GetRoles(ctx context.Context, accountRef, orgRef primitive.ObjectID) ([]model.Role, error) { + sub := accountRef.Hex() + dom := orgRef.Hex() + + c.logger.Debug("Fetching roles for user", zap.String("subject", sub), zap.String("domain", dom)) + + // Get all roles for the user in the domain + sroles, err := c.enforcer.GetFilteredGroupingPolicy(0, sub, "", dom) + if err != nil { + c.logger.Warn("Failed to get roles from policies", zap.Error(err), + zap.String("account_ref", sub), zap.String("organization_ref", dom), + ) + return nil, merrors.Internal("failed to fetch roles from policies") + } + + roles := make([]model.Role, 0, len(sroles)) + for _, srole := range sroles { + role, err := c.roleSerializer.Deserialize(srole) + if err != nil { + c.logger.Warn("Failed to deserialize role", zap.Error(err)) + return nil, err + } + roles = append(roles, *role) + } + + c.logger.Debug("Roles fetched successfully", zap.Int("count", len(roles))) + return roles, nil +} + +// GetPermissions retrieves all effective policies for the user within the domain. +func (c *CasbinEnforcer) GetPermissions(ctx context.Context, accountRef, orgRef primitive.ObjectID) ([]model.Role, []model.Permission, error) { + c.logger.Debug("Fetching policies for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef)) + + // Step 1: Retrieve all roles assigned to the user within the domain + roles, err := c.GetRoles(ctx, accountRef, orgRef) + if err != nil { + c.logger.Warn("Failed to get roles", zap.Error(err)) + return nil, nil, err + } + + // Map to hold unique policies + permissionsMap := make(map[string]*model.Permission) + for _, role := range roles { + // Step 2a: Retrieve all policies associated with the role within the domain + policies, err := c.enforcer.GetFilteredPolicy(0, role.DescriptionRef.Hex()) + if err != nil { + c.logger.Warn("Failed to get policies for role", zap.Error(err), mzap.ObjRef("role_ref", role.DescriptionRef)) + continue + } + + // Step 2b: Process each policy to extract Permission, Action, and Effect + for _, policy := range policies { + + if len(policy) < 5 { + c.logger.Warn("Incomplete policy encountered", zap.Strings("policy", policy)) + continue // Ensure the policy line has enough fields + } + + // Deserialize the policy using + deserializedPolicy, err := c.permissionSerializer.Deserialize(policy) + if err != nil { + c.logger.Warn("Failed to deserialize policy", zap.Error(err), zap.Strings("policy", policy)) + continue + } + + // Construct a unique key combining Permission ID and Action to prevent duplicates + policyKey := deserializedPolicy.DescriptionRef.Hex() + ":" + string(deserializedPolicy.Effect.Action) + if _, exists := permissionsMap[policyKey]; exists { + continue // Policy-action pair already accounted for + } + + // Add the Policy to the map + permissionsMap[policyKey] = &model.Permission{ + RolePolicy: *deserializedPolicy, + AccountRef: accountRef, + } + c.logger.Debug("Policy added to policyMap", zap.Any("policy_key", policyKey)) + } + } + + // Convert the map to a slice + permissions := make([]model.Permission, 0, len(permissionsMap)) + for _, permission := range permissionsMap { + permissions = append(permissions, *permission) + } + + c.logger.Debug("Permissions fetched successfully", zap.Int("count", len(permissions))) + return roles, permissions, nil +} diff --git a/api/pkg/auth/internal/casbin/factory.go b/api/pkg/auth/internal/casbin/factory.go new file mode 100644 index 0000000..82de7d1 --- /dev/null +++ b/api/pkg/auth/internal/casbin/factory.go @@ -0,0 +1,34 @@ +package casbin + +import ( + "github.com/casbin/casbin/v2" + mongodbadapter "github.com/casbin/mongodb-adapter/v3" + cc "github.com/tech/sendico/pkg/auth/internal/casbin/config" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +func createAdapter(logger mlogger.Logger, config *cc.Config, client *mongo.Client) (*casbin.Enforcer, error) { + dbc, err := cc.PrepareConfig(logger, config) + if err != nil { + logger.Warn("Failed to prepare database configuration", zap.Error(err)) + return nil, err + } + + adapter, err := mongodbadapter.NewAdapterByDB(client, dbc.Adapter) + if err != nil { + logger.Warn("Failed to create DB adapter", zap.Error(err)) + return nil, err + } + + e, err := casbin.NewEnforcer(dbc.ModelPath, adapter, NewCasbinLogger(logger)) + if err != nil { + logger.Warn("Failed to create permissions enforcer", zap.Error(err)) + return nil, err + } + e.EnableAutoSave(true) + + // No need to manually load policy. Casbin does it for us + return e, nil +} diff --git a/api/pkg/auth/internal/casbin/logger.go b/api/pkg/auth/internal/casbin/logger.go new file mode 100644 index 0000000..1a172bd --- /dev/null +++ b/api/pkg/auth/internal/casbin/logger.go @@ -0,0 +1,61 @@ +package casbin + +import ( + "strings" + + "github.com/tech/sendico/pkg/mlogger" + "go.uber.org/zap" +) + +// CasbinZapLogger wraps a zap.Logger to implement Casbin's Logger interface. +type CasbinZapLogger struct { + logger mlogger.Logger +} + +// NewCasbinLogger constructs a new CasbinZapLogger. +func NewCasbinLogger(logger mlogger.Logger) *CasbinZapLogger { + return &CasbinZapLogger{ + logger: logger.Named("driver"), + } +} + +// EnableLog enables or disables logging. +func (l *CasbinZapLogger) EnableLog(_ bool) { + // ignore +} + +// IsEnabled returns whether logging is currently enabled. +func (l *CasbinZapLogger) IsEnabled() bool { + return true +} + +// LogModel is called by Casbin when loading model settings (you can customize if you want). +func (l *CasbinZapLogger) LogModel(m [][]string) { + l.logger.Info("Model loaded", zap.Any("model", m)) +} + +func (l *CasbinZapLogger) LogPolicy(m map[string][][]string) { + l.logger.Info("Policy loaded", zap.Int("entries", len(m))) +} + +func (l *CasbinZapLogger) LogError(err error, msg ...string) { + // If no custom message was passed, log a generic one + if len(msg) == 0 { + l.logger.Warn("Error occurred", zap.Error(err)) + return + } + + // Otherwise, join any provided messages and include them + l.logger.Warn(strings.Join(msg, " "), zap.Error(err)) +} + +// LogEnforce is called by Casbin to log each Enforce() call if logging is enabled. +func (l *CasbinZapLogger) LogEnforce(matcher string, request []any, result bool, explains [][]string) { + l.logger.Debug("Enforcing policy...", zap.String("matcher", matcher), zap.Any("request", request), + zap.Bool("result", result), zap.Any("explains", explains)) +} + +// LogRole is called by Casbin when role manager adds or deletes a role. +func (l *CasbinZapLogger) LogRole(roles []string) { + l.logger.Debug("Changing roles...", zap.Strings("roles", roles)) +} diff --git a/api/pkg/auth/internal/casbin/manager.go b/api/pkg/auth/internal/casbin/manager.go new file mode 100644 index 0000000..20b851e --- /dev/null +++ b/api/pkg/auth/internal/casbin/manager.go @@ -0,0 +1,54 @@ +// package casbin + +package casbin + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/management" + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/role" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +// CasbinManager implements the auth.Manager interface by aggregating Role and Permission managers. +type CasbinManager struct { + logger mlogger.Logger + roleManager management.Role + permManager management.Permission +} + +// NewManager creates a new CasbinManager with specified domains and role-domain mappings. +func NewManager( + l mlogger.Logger, + pdb policy.DB, + rdb role.DB, + enforcer *CasbinEnforcer, + settings model.SettingsT, +) (*CasbinManager, error) { + logger := l.Named("manager") + + var pdesc model.PolicyDescription + if err := pdb.GetBuiltInPolicy(context.Background(), "roles", &pdesc); err != nil { + logger.Warn("Failed to fetch roles permission reference", zap.Error(err)) + return nil, err + } + + return &CasbinManager{ + logger: logger, + roleManager: NewRoleManager(logger, enforcer, pdesc.ID, rdb), + permManager: NewPermissionManager(logger, enforcer), + }, nil +} + +// Permission returns the Permission manager. +func (m *CasbinManager) Permission() management.Permission { + return m.permManager +} + +// Role returns the Role manager. +func (m *CasbinManager) Role() management.Role { + return m.roleManager +} diff --git a/api/pkg/auth/internal/casbin/models/auth.conf b/api/pkg/auth/internal/casbin/models/auth.conf new file mode 100644 index 0000000..3dc5574 --- /dev/null +++ b/api/pkg/auth/internal/casbin/models/auth.conf @@ -0,0 +1,54 @@ +###################################################### +# Request Definition +###################################################### +[request_definition] +# Explanation: +# - `accountRef`: The account (user) making the request. +# - `organizationRef`: The organization in which the role applies. +# - `permissionRef`: The specific permission being requested. +# - `objectRef`: The object/resource being accessed (specific object or all objects). +# - `action`: The action being requested (CRUD: read, write, update, delete). +r = accountRef, organizationRef, permissionRef, objectRef, action + + +###################################################### +# Policy Definition +###################################################### +[policy_definition] +# Explanation: +# - `roleRef`: The role to which the policy is assigned. +# - `organizationRef`: The organization in which the role applies. +# - `permissionRef`: The permission associated with the policy. +# - `objectRef`: The specific object/resource the policy applies to (or all objects). +# - `action`: The CRUD action permitted or denied. +# - `eft`: Effect of the policy (`allow` or `deny`). +p = roleRef, organizationRef, permissionRef, objectRef, action, eft + + +###################################################### +# Role Definition +###################################################### +[role_definition] +# Explanation: +# - Maps `accountRef` (user) to `roleRef` (role) within `organizationRef` (scope). +# Casbin requires underscores for placeholders, so we do not literally use accountRef, roleRef, etc. here. +g = _, _, _ + + +###################################################### +# Policy Effect +###################################################### +[policy_effect] +# Explanation: +# - Grants access if any `allow` policy matches and no `deny` policies match. +e = some(where (p.eft == allow)) && !some(where (p.eft == deny)) + + +###################################################### +# Matchers +###################################################### +[matchers] +# Explanation: +# - Checks if the user (accountRef) belongs to the roleRef within an organizationRef via `g()`. +# - Ensures the organizationRef, permissionRef, objectRef, and action match the policy. +m = g(r.accountRef, p.roleRef, r.organizationRef) && r.organizationRef == p.organizationRef && r.permissionRef == p.permissionRef && (p.objectRef == r.objectRef || p.objectRef == "*") && r.action == p.action diff --git a/api/pkg/auth/internal/casbin/permissions.go b/api/pkg/auth/internal/casbin/permissions.go new file mode 100644 index 0000000..6240765 --- /dev/null +++ b/api/pkg/auth/internal/casbin/permissions.go @@ -0,0 +1,167 @@ +package casbin + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/anyobject" + "github.com/tech/sendico/pkg/auth/internal/casbin/serialization" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// CasbinPermissionManager manages permissions using Casbin. +type CasbinPermissionManager struct { + logger mlogger.Logger // Logger for logging operations + enforcer *CasbinEnforcer // Casbin enforcer for managing policies + serializer serialization.Policy // Serializer for converting policies to/from Casbin +} + +// GrantToRole adds a permission to a role in Casbin. +func (m *CasbinPermissionManager) GrantToRole(ctx context.Context, policy *model.RolePolicy) error { + objRef := anyobject.ID + if (policy.ObjectRef != nil) && (*policy.ObjectRef != primitive.NilObjectID) { + objRef = policy.ObjectRef.Hex() + } + + m.logger.Debug("Granting permission to role", + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + zap.String("object_ref", objRef), + zap.String("action", string(policy.Effect.Action)), + zap.String("effect", string(policy.Effect.Effect)), + ) + + // Serialize permission + serializedPolicy, err := m.serializer.Serialize(policy) + if err != nil { + m.logger.Error("Failed to serialize permission while granting permission", zap.Error(err), + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + mzap.ObjRef("organization_ref", policy.OrganizationRef), + ) + return err + } + + // Add policy to Casbin + added, err := m.enforcer.enforcer.AddPolicy(serializedPolicy...) + if err != nil { + m.logger.Error("Failed to add policy to Casbin", zap.Error(err)) + return err + } + if added { + m.logger.Info("Policy added to Casbin", + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + zap.String("object_ref", objRef), + ) + } else { + m.logger.Warn("Policy already exists in Casbin", + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + zap.String("object_ref", objRef), + ) + } + + return nil +} + +// RevokeFromRole removes a permission from a role in Casbin. +func (m *CasbinPermissionManager) RevokeFromRole(ctx context.Context, policy *model.RolePolicy) error { + objRef := anyobject.ID + if policy.ObjectRef != nil { + objRef = policy.ObjectRef.Hex() + } + m.logger.Debug("Revoking permission from role", + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + zap.String("object_ref", objRef), + zap.String("action", string(policy.Effect.Action)), + zap.String("effect", string(policy.Effect.Effect)), + ) + + // Serialize policy + serializedPolicy, err := m.serializer.Serialize(policy) + if err != nil { + m.logger.Error("Failed to serialize policy while revoking permission from role", + zap.Error(err), mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("policy_ref", policy.DescriptionRef)) + return err + } + + // Remove policy from Casbin + removed, err := m.enforcer.enforcer.RemovePolicy(serializedPolicy...) + if err != nil { + m.logger.Error("Failed to remove policy from Casbin", zap.Error(err)) + return err + } + if removed { + m.logger.Info("Policy removed from Casbin", + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + zap.String("object_ref", objRef), + ) + } else { + m.logger.Warn("Policy does not exist in Casbin", + mzap.ObjRef("role_ref", policy.RoleDescriptionRef), + mzap.ObjRef("permission_ref", policy.DescriptionRef), + zap.String("object_ref", objRef), + ) + } + + return nil +} + +// GetPolicies retrieves all policies for a specific role. +func (m *CasbinPermissionManager) GetPolicies( + ctx context.Context, + roleRef primitive.ObjectID, +) ([]model.RolePolicy, error) { + m.logger.Debug("Fetching policies for role", mzap.ObjRef("role_ref", roleRef)) + + // Retrieve Casbin policies for the role + policies, err := m.enforcer.enforcer.GetFilteredPolicy(0, roleRef.Hex()) + if err != nil { + m.logger.Warn("Failed to get policies", zap.Error(err), mzap.ObjRef("role_ref", roleRef)) + return nil, err + } + if len(policies) == 0 { + m.logger.Info("No policies found for role", mzap.ObjRef("role_ref", roleRef)) + return nil, merrors.NoData("no policies") + } + + // Deserialize policies + var result []model.RolePolicy + for _, policy := range policies { + permission, err := m.serializer.Deserialize(policy) + if err != nil { + m.logger.Warn("Failed to deserialize policy", zap.Error(err), zap.String("policy", policy[0])) + continue + } + result = append(result, *permission) + } + + m.logger.Debug("Policies fetched successfully", mzap.ObjRef("role_ref", roleRef), zap.Int("count", len(result))) + return result, nil +} + +// Save persists changes to the Casbin policy store. +func (m *CasbinPermissionManager) Save() error { + if err := m.enforcer.enforcer.SavePolicy(); err != nil { + m.logger.Error("Failed to save policies in Casbin", zap.Error(err)) + return err + } + m.logger.Info("Policies successfully saved in Casbin") + return nil +} + +func NewPermissionManager(logger mlogger.Logger, enforcer *CasbinEnforcer) *CasbinPermissionManager { + return &CasbinPermissionManager{ + logger: logger.Named("permission"), + enforcer: enforcer, + serializer: serialization.NewPolicySerializer(), + } +} diff --git a/api/pkg/auth/internal/casbin/role.go b/api/pkg/auth/internal/casbin/role.go new file mode 100644 index 0000000..cc42979 --- /dev/null +++ b/api/pkg/auth/internal/casbin/role.go @@ -0,0 +1,209 @@ +package casbin + +import ( + "context" + + "github.com/tech/sendico/pkg/db/role" + "github.com/tech/sendico/pkg/db/storable" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.uber.org/zap" +) + +// RoleManager manages roles using Casbin. +type RoleManager struct { + logger mlogger.Logger + enforcer *CasbinEnforcer + rdb role.DB + rolePermissionRef primitive.ObjectID +} + +// NewRoleManager creates a new RoleManager. +func NewRoleManager(logger mlogger.Logger, enforcer *CasbinEnforcer, rolePermissionRef primitive.ObjectID, rdb role.DB) *RoleManager { + return &RoleManager{ + logger: logger.Named("role"), + enforcer: enforcer, + rdb: rdb, + rolePermissionRef: rolePermissionRef, + } +} + +// validateObjectIDs ensures that all provided ObjectIDs are non-zero. +func (rm *RoleManager) validateObjectIDs(ids ...primitive.ObjectID) error { + for _, id := range ids { + if id.IsZero() { + return merrors.InvalidArgument("Object references cannot be zero") + } + } + return nil +} + +// removePolicies removes policies based on the provided filter and logs the results. +func (rm *RoleManager) removePolicies(policyType, role string, roleRef primitive.ObjectID) error { + filterIndex := 1 + if policyType == "permission" { + filterIndex = 0 + } + policies, err := rm.enforcer.enforcer.GetFilteredPolicy(filterIndex, role) + if err != nil { + rm.logger.Warn("Failed to fetch "+policyType+" policies", zap.Error(err), mzap.ObjRef("role_ref", roleRef)) + return err + } + + for _, policy := range policies { + args := make([]any, len(policy)) + for i, v := range policy { + args[i] = v + } + var removed bool + var removeErr error + if policyType == "grouping" { + removed, removeErr = rm.enforcer.enforcer.RemoveGroupingPolicy(args...) + } else { + removed, removeErr = rm.enforcer.enforcer.RemovePolicy(args...) + } + + if removeErr != nil { + rm.logger.Warn("Failed to remove "+policyType+" policy for role", zap.Error(removeErr), mzap.ObjRef("role_ref", roleRef), zap.Strings("policy", policy)) + return removeErr + } + if removed { + rm.logger.Info("Removed "+policyType+" policy for role", mzap.ObjRef("role_ref", roleRef), zap.Strings("policy", policy)) + } + } + return nil +} + +// fetchRolesFromPolicies retrieves and converts policies to roles. +func (rm *RoleManager) fetchRolesFromPolicies(policies [][]string, orgRef primitive.ObjectID) []model.RoleDescription { + roles := make([]model.RoleDescription, 0, len(policies)) + for _, policy := range policies { + if len(policy) < 2 { + continue + } + + roleID, err := primitive.ObjectIDFromHex(policy[1]) + if err != nil { + rm.logger.Warn("Invalid role ID", zap.String("roleID", policy[1])) + continue + } + roles = append(roles, model.RoleDescription{Base: storable.Base{ID: roleID}, OrganizationRef: orgRef}) + } + return roles +} + +// Create creates a new role in an organization. +func (rm *RoleManager) Create(ctx context.Context, orgRef primitive.ObjectID, description *model.Describable) (*model.RoleDescription, error) { + if err := rm.validateObjectIDs(orgRef); err != nil { + return nil, err + } + + role := &model.RoleDescription{ + Describable: *description, + OrganizationRef: orgRef, + } + if err := rm.rdb.Create(ctx, role); err != nil { + rm.logger.Warn("Failed to create role", zap.Error(err), mzap.ObjRef("organiztion_ref", orgRef)) + return nil, err + } + + rm.logger.Info("Role created successfully", mzap.StorableRef(role), mzap.ObjRef("organization_ref", orgRef)) + return role, nil +} + +// Assign assigns a role to a user in the given organization. +func (rm *RoleManager) Assign(ctx context.Context, role *model.Role) error { + if err := rm.validateObjectIDs(role.DescriptionRef, role.AccountRef, role.OrganizationRef); err != nil { + return err + } + + sub := role.AccountRef.Hex() + roleID := role.DescriptionRef.Hex() + domain := role.OrganizationRef.Hex() + + added, err := rm.enforcer.enforcer.AddGroupingPolicy(sub, roleID, domain) + return rm.logPolicyResult("assign", added, err, role.DescriptionRef, role.AccountRef, role.OrganizationRef) +} + +// Delete removes a role entirely and cleans up associated Casbin policies. +func (rm *RoleManager) Delete(ctx context.Context, roleRef primitive.ObjectID) error { + if err := rm.validateObjectIDs(roleRef); err != nil { + rm.logger.Warn("Failed to delete role", mzap.ObjRef("role_ref", roleRef)) + return err + } + + if err := rm.rdb.Delete(ctx, roleRef); err != nil { + rm.logger.Warn("Failed to delete role", mzap.ObjRef("role_ref", roleRef)) + return err + } + + role := roleRef.Hex() + + // Remove grouping policies + if err := rm.removePolicies("grouping", role, roleRef); err != nil { + return err + } + + // Remove permission policies + if err := rm.removePolicies("permission", role, roleRef); err != nil { + return err + } + + // // Save changes + // if err := rm.enforcer.enforcer.SavePolicy(); err != nil { + // rm.logger.Warn("Failed to save Casbin policies after role deletion", + // zap.Error(err), + // mzap.ObjRef("role_ref", roleRef), + // ) + // return err + // } + + rm.logger.Info("Role deleted successfully along with associated policies", mzap.ObjRef("role_ref", roleRef)) + return nil +} + +// Revoke removes a role from a user. +func (rm *RoleManager) Revoke(ctx context.Context, roleRef, accountRef, orgRef primitive.ObjectID) error { + if err := rm.validateObjectIDs(roleRef, accountRef, orgRef); err != nil { + return err + } + + sub := accountRef.Hex() + role := roleRef.Hex() + domain := orgRef.Hex() + + removed, err := rm.enforcer.enforcer.RemoveGroupingPolicy(sub, role, domain) + return rm.logPolicyResult("revoke", removed, err, roleRef, accountRef, orgRef) +} + +// logPolicyResult logs results for Assign and Revoke. +func (rm *RoleManager) logPolicyResult(action string, result bool, err error, roleRef, accountRef, orgRef primitive.ObjectID) error { + if err != nil { + rm.logger.Warn("Failed to "+action+" role", zap.Error(err), mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef)) + return err + } + msg := "Role " + action + "ed successfully" + if !result { + msg = "Role already " + action + "ed" + } + rm.logger.Info(msg, mzap.ObjRef("role_ref", roleRef), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", orgRef)) + return nil +} + +// List retrieves all roles in an organization or all roles if orgRef is zero. +func (rm *RoleManager) List(ctx context.Context, orgRef primitive.ObjectID) ([]model.RoleDescription, error) { + domain := orgRef.Hex() + groupingPolicies, err := rm.enforcer.enforcer.GetFilteredGroupingPolicy(2, domain) + if err != nil { + rm.logger.Warn("Failed to fetch grouping policies", zap.Error(err), mzap.ObjRef("organization_ref", orgRef)) + return nil, err + } + + roles := rm.fetchRolesFromPolicies(groupingPolicies, orgRef) + + rm.logger.Info("Retrieved roles for organization", mzap.ObjRef("organization_ref", orgRef), zap.Int("count", len(roles))) + return roles, nil +} diff --git a/api/pkg/auth/internal/casbin/serialization/internal/policy.go b/api/pkg/auth/internal/casbin/serialization/internal/policy.go new file mode 100644 index 0000000..65b7ab5 --- /dev/null +++ b/api/pkg/auth/internal/casbin/serialization/internal/policy.go @@ -0,0 +1,81 @@ +package serializationimp + +import ( + "github.com/tech/sendico/pkg/auth/anyobject" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// PolicySerializer implements CasbinSerializer for Permission. +type PolicySerializer struct{} + +// Serialize converts a Permission object into a Casbin policy. +func (s *PolicySerializer) Serialize(entity *model.RolePolicy) ([]any, error) { + if entity.RoleDescriptionRef.IsZero() || + entity.OrganizationRef.IsZero() || + entity.DescriptionRef.IsZero() || // Ensure permissionRef is valid + entity.Effect.Action == "" || // Ensure action is not empty + entity.Effect.Effect == "" { // Ensure effect (eft) is not empty + return nil, merrors.InvalidArgument("permission contains invalid object references or missing fields") + } + + objectRef := anyobject.ID + if entity.ObjectRef != nil { + objectRef = entity.ObjectRef.Hex() + } + + return []any{ + entity.RoleDescriptionRef.Hex(), // Maps to p.roleRef + entity.OrganizationRef.Hex(), // Maps to p.organizationRef + entity.DescriptionRef.Hex(), // Maps to p.permissionRef + objectRef, // Maps to p.objectRef (wildcard if empty) + string(entity.Effect.Action), // Maps to p.action + string(entity.Effect.Effect), // Maps to p.eft + }, nil +} + +// Deserialize converts a Casbin policy into a Permission object. +func (s *PolicySerializer) Deserialize(policy []string) (*model.RolePolicy, error) { + if len(policy) != 6 { // Ensure policy has the correct number of fields + return nil, merrors.Internal("invalid policy format") + } + + roleRef, err := primitive.ObjectIDFromHex(policy[0]) + if err != nil { + return nil, merrors.InvalidArgument("invalid roleRef in policy") + } + + organizationRef, err := primitive.ObjectIDFromHex(policy[1]) + if err != nil { + return nil, merrors.InvalidArgument("invalid organizationRef in policy") + } + + permissionRef, err := primitive.ObjectIDFromHex(policy[2]) + if err != nil { + return nil, merrors.InvalidArgument("invalid permissionRef in policy") + } + + // Handle wildcard for ObjectRef + var objectRef *primitive.ObjectID + if policy[3] != anyobject.ID { + ref, err := primitive.ObjectIDFromHex(policy[3]) + if err != nil { + return nil, merrors.InvalidArgument("invalid objectRef in policy") + } + objectRef = &ref + } + + return &model.RolePolicy{ + RoleDescriptionRef: roleRef, + Policy: model.Policy{ + OrganizationRef: organizationRef, + DescriptionRef: permissionRef, + ObjectRef: objectRef, + Effect: model.ActionEffect{ + Action: model.Action(policy[4]), + Effect: model.Effect(policy[5]), + }, + }, + }, nil +} diff --git a/api/pkg/auth/internal/casbin/serialization/internal/role.go b/api/pkg/auth/internal/casbin/serialization/internal/role.go new file mode 100644 index 0000000..e58c36b --- /dev/null +++ b/api/pkg/auth/internal/casbin/serialization/internal/role.go @@ -0,0 +1,57 @@ +package serializationimp + +import ( + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// RoleSerializer implements CasbinSerializer for Role. +type RoleSerializer struct{} + +// Serialize converts a Role object into a Casbin grouping policy. +func (s *RoleSerializer) Serialize(entity *model.Role) ([]any, error) { + // Validate required fields + if entity.AccountRef.IsZero() || entity.DescriptionRef.IsZero() || entity.OrganizationRef.IsZero() { + return nil, merrors.InvalidArgument("role contains invalid object references") + } + + return []any{ + entity.AccountRef.Hex(), // Maps to g(_, _, _) accountRef + entity.DescriptionRef.Hex(), // Maps to g(_, _, _) roleRef + entity.OrganizationRef.Hex(), // Maps to g(_, _, _) organizationRef + }, nil +} + +// Deserialize converts a Casbin grouping policy into a Role object. +func (s *RoleSerializer) Deserialize(policy []string) (*model.Role, error) { + // Ensure the policy has exactly 3 fields + if len(policy) != 3 { + return nil, merrors.Internal("invalid grouping policy format") + } + + // Parse accountRef + accountRef, err := primitive.ObjectIDFromHex(policy[0]) + if err != nil { + return nil, merrors.InvalidArgument("invalid accountRef in grouping policy") + } + + // Parse roleDescriptionRef (roleRef) + roleDescriptionRef, err := primitive.ObjectIDFromHex(policy[1]) + if err != nil { + return nil, merrors.InvalidArgument("invalid roleRef in grouping policy") + } + + // Parse organizationRef + organizationRef, err := primitive.ObjectIDFromHex(policy[2]) + if err != nil { + return nil, merrors.InvalidArgument("invalid organizationRef in grouping policy") + } + + // Return the constructed Role object + return &model.Role{ + AccountRef: accountRef, + DescriptionRef: roleDescriptionRef, + OrganizationRef: organizationRef, + }, nil +} diff --git a/api/pkg/auth/internal/casbin/serialization/policy.go b/api/pkg/auth/internal/casbin/serialization/policy.go new file mode 100644 index 0000000..0e1fa29 --- /dev/null +++ b/api/pkg/auth/internal/casbin/serialization/policy.go @@ -0,0 +1,12 @@ +package serialization + +import ( + serializationimp "github.com/tech/sendico/pkg/auth/internal/casbin/serialization/internal" + "github.com/tech/sendico/pkg/model" +) + +type Policy = CasbinSerializer[model.RolePolicy] + +func NewPolicySerializer() Policy { + return &serializationimp.PolicySerializer{} +} diff --git a/api/pkg/auth/internal/casbin/serialization/role.go b/api/pkg/auth/internal/casbin/serialization/role.go new file mode 100644 index 0000000..59ea1dc --- /dev/null +++ b/api/pkg/auth/internal/casbin/serialization/role.go @@ -0,0 +1,12 @@ +package serialization + +import ( + serializationimp "github.com/tech/sendico/pkg/auth/internal/casbin/serialization/internal" + "github.com/tech/sendico/pkg/model" +) + +type Role = CasbinSerializer[model.Role] + +func NewRoleSerializer() Role { + return &serializationimp.RoleSerializer{} +} diff --git a/api/pkg/auth/internal/casbin/serialization/serializer.go b/api/pkg/auth/internal/casbin/serialization/serializer.go new file mode 100644 index 0000000..36d11db --- /dev/null +++ b/api/pkg/auth/internal/casbin/serialization/serializer.go @@ -0,0 +1,10 @@ +package serialization + +// CasbinSerializer defines methods for serializing and deserializing any Casbin-compatible entity. +type CasbinSerializer[T any] interface { + // Serialize converts an entity (Role or Permission) into a Casbin policy. + Serialize(entity *T) ([]any, error) + + // Deserialize converts a Casbin policy into an entity (Role or Permission). + Deserialize(policy []string) (*T, error) +} diff --git a/api/pkg/auth/internal/native/db/policies.go b/api/pkg/auth/internal/native/db/policies.go new file mode 100644 index 0000000..778f609 --- /dev/null +++ b/api/pkg/auth/internal/native/db/policies.go @@ -0,0 +1,151 @@ +package db + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/internal/native/nstructures" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + mutil "github.com/tech/sendico/pkg/mutil/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type PermissionsDBImp struct { + template.DBImp[*nstructures.PolicyAssignment] +} + +func (db *PermissionsDBImp) Policies(ctx context.Context, object model.PermissionBoundStorable, action model.Action) ([]nstructures.PolicyAssignment, error) { + return mutil.GetObjects[nstructures.PolicyAssignment]( + ctx, + db.Logger, + repository.Query().And( + repository.Filter("policy.organizationRef", object.GetOrganizationRef()), + repository.Filter("policy.descriptionRef", object.GetPermissionRef()), + repository.Filter("policy.effect.action", action), + repository.Query().Or( + repository.Filter("policy.objectRef", *object.GetID()), + repository.Filter("policy.objectRef", nil), + ), + ), + nil, + db.Repository, + ) +} + +func (db *PermissionsDBImp) PoliciesForPermissionAction(ctx context.Context, roleRef, permissionRef primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) { + return mutil.GetObjects[nstructures.PolicyAssignment]( + ctx, + db.Logger, + repository.Query().And( + repository.Filter("roleRef", roleRef), + repository.Filter("policy.descriptionRef", permissionRef), + repository.Filter("policy.effect.action", action), + ), + nil, + db.Repository, + ) +} + +func (db *PermissionsDBImp) Remove(ctx context.Context, policy *model.RolePolicy) error { + objRefFilter := repository.Query().Or( + repository.Filter("policy.objectRef", nil), + repository.Filter("policy.objectRef", primitive.NilObjectID), + ) + if policy.ObjectRef != nil { + objRefFilter = repository.Filter("policy.objectRef", *policy.ObjectRef) + } + return db.Repository.DeleteMany( + ctx, + repository.Query().And( + repository.Filter("roleRef", policy.RoleDescriptionRef), + repository.Filter("policy.organizationRef", policy.OrganizationRef), + repository.Filter("policy.descriptionRef", policy.DescriptionRef), + objRefFilter, + repository.Filter("policy.effect.action", policy.Effect.Action), + repository.Filter("policy.effect.effect", policy.Effect.Effect), + ), + ) +} + +func (db *PermissionsDBImp) PoliciesForRole(ctx context.Context, roleRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) { + return mutil.GetObjects[nstructures.PolicyAssignment]( + ctx, + db.Logger, + repository.Filter("roleRef", roleRef), + nil, + db.Repository, + ) +} + +func (db *PermissionsDBImp) PoliciesForRoles(ctx context.Context, roleRefs []primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) { + if len(roleRefs) == 0 { + db.Logger.Debug("Empty role references list provided, returning empty resposnse") + return []nstructures.PolicyAssignment{}, nil + } + return mutil.GetObjects[nstructures.PolicyAssignment]( + ctx, + db.Logger, + repository.Query().And( + repository.Query().In(repository.Field("roleRef"), roleRefs), + repository.Filter("policy.effect.action", action), + ), + nil, + db.Repository, + ) +} + +func NewPoliciesDB(logger mlogger.Logger, db *mongo.Database) (*PermissionsDBImp, error) { + p := &PermissionsDBImp{ + DBImp: *template.Create[*nstructures.PolicyAssignment](logger, mservice.PolicyAssignements, db), + } + + // faster + // harder + // index + policiesQueryIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "policy.organizationRef", Sort: ri.Asc}, + {Field: "policy.descriptionRef", Sort: ri.Asc}, + {Field: "policy.effect.action", Sort: ri.Asc}, + {Field: "policy.objectRef", Sort: ri.Asc}, + }, + } + if err := p.DBImp.Repository.CreateIndex(policiesQueryIndex); err != nil { + p.Logger.Warn("Failed to prepare policies query index", zap.Error(err)) + return nil, err + } + + roleBasedQueriesIndex := &ri.Definition{ + Keys: []ri.Key{ + {Field: "roleRef", Sort: ri.Asc}, + {Field: "policy.effect.action", Sort: ri.Asc}, + }, + } + if err := p.DBImp.Repository.CreateIndex(roleBasedQueriesIndex); err != nil { + p.Logger.Warn("Failed to prepare role based query index", zap.Error(err)) + return nil, err + } + + uniquePolicyConstaint := &ri.Definition{ + Keys: []ri.Key{ + {Field: "policy.organizationRef", Sort: ri.Asc}, + {Field: "roleRef", Sort: ri.Asc}, + {Field: "policy.descriptionRef", Sort: ri.Asc}, + {Field: "policy.effect.action", Sort: ri.Asc}, + {Field: "policy.objectRef", Sort: ri.Asc}, + }, + Unique: true, + } + if err := p.DBImp.Repository.CreateIndex(uniquePolicyConstaint); err != nil { + p.Logger.Warn("Failed to unique policy assignment index", zap.Error(err)) + return nil, err + } + + return p, nil +} diff --git a/api/pkg/auth/internal/native/db/roles.go b/api/pkg/auth/internal/native/db/roles.go new file mode 100644 index 0000000..8ad37d5 --- /dev/null +++ b/api/pkg/auth/internal/native/db/roles.go @@ -0,0 +1,99 @@ +package db + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/internal/native/nstructures" + "github.com/tech/sendico/pkg/db/repository" + ri "github.com/tech/sendico/pkg/db/repository/index" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + mutil "github.com/tech/sendico/pkg/mutil/db" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type RolesDBImp struct { + template.DBImp[*nstructures.RoleAssignment] +} + +func (db *RolesDBImp) Roles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) { + return mutil.GetObjects[nstructures.RoleAssignment]( + ctx, + db.Logger, + repository.Query().And( + repository.Filter("role.accountRef", accountRef), + repository.Filter("role.organizationRef", organizationRef), + ), + nil, + db.Repository, + ) +} + +func (db *RolesDBImp) RolesForVenue(ctx context.Context, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) { + return mutil.GetObjects[nstructures.RoleAssignment]( + ctx, + db.Logger, + repository.Query().And( + repository.Filter("role.organizationRef", organizationRef), + ), + nil, + db.Repository, + ) +} + +func (db *RolesDBImp) DeleteRole(ctx context.Context, roleRef primitive.ObjectID) error { + return db.DeleteMany( + ctx, + repository.Query().And( + repository.Filter("role.descriptionRef", roleRef), + ), + ) +} + +func (db *RolesDBImp) RemoveRole(ctx context.Context, roleRef, organizationRef, accountRef primitive.ObjectID) error { + return db.DeleteMany( + ctx, + repository.Query().And( + repository.Filter("role.accountRef", accountRef), + repository.Filter("role.organizationRef", organizationRef), + repository.Filter("role.descriptionRef", roleRef), + ), + ) +} + +func NewRolesDB(logger mlogger.Logger, db *mongo.Database) (*RolesDBImp, error) { + p := &RolesDBImp{ + DBImp: *template.Create[*nstructures.RoleAssignment](logger, "role_assignments", db), + } + + if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ + Keys: []ri.Key{{Field: "role.organizationRef", Sort: ri.Asc}}, + }); err != nil { + p.Logger.Warn("Failed to prepare venue index", zap.Error(err)) + return nil, err + } + + if err := p.DBImp.Repository.CreateIndex(&ri.Definition{ + Keys: []ri.Key{{Field: "role.descriptionRef", Sort: ri.Asc}}, + }); err != nil { + p.Logger.Warn("Failed to prepare role description index", zap.Error(err)) + return nil, err + } + + uniqueRoleConstaint := &ri.Definition{ + Keys: []ri.Key{ + {Field: "role.organizationRef", Sort: ri.Asc}, + {Field: "role.accountRef", Sort: ri.Asc}, + {Field: "role.descriptionRef", Sort: ri.Asc}, + }, + Unique: true, + } + if err := p.DBImp.Repository.CreateIndex(uniqueRoleConstaint); err != nil { + p.Logger.Warn("Failed to prepare role assignment index", zap.Error(err)) + return nil, err + } + + return p, nil +} diff --git a/api/pkg/auth/internal/native/dbpolicies.go b/api/pkg/auth/internal/native/dbpolicies.go new file mode 100644 index 0000000..663a7fd --- /dev/null +++ b/api/pkg/auth/internal/native/dbpolicies.go @@ -0,0 +1,27 @@ +package native + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/internal/native/db" + "github.com/tech/sendico/pkg/auth/internal/native/nstructures" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type PoliciesDB interface { + template.DB[*nstructures.PolicyAssignment] + // plenty of interfaces for performance reasons + Policies(ctx context.Context, object model.PermissionBoundStorable, action model.Action) ([]nstructures.PolicyAssignment, error) + PoliciesForPermissionAction(ctx context.Context, roleRef, permissionRef primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) + PoliciesForRole(ctx context.Context, roleRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) + PoliciesForRoles(ctx context.Context, roleRefs []primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) + Remove(ctx context.Context, policy *model.RolePolicy) error +} + +func NewPoliciesDBDB(logger mlogger.Logger, conn *mongo.Database) (PoliciesDB, error) { + return db.NewPoliciesDB(logger, conn) +} diff --git a/api/pkg/auth/internal/native/dbroles.go b/api/pkg/auth/internal/native/dbroles.go new file mode 100644 index 0000000..104dadd --- /dev/null +++ b/api/pkg/auth/internal/native/dbroles.go @@ -0,0 +1,24 @@ +package native + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/internal/native/db" + "github.com/tech/sendico/pkg/auth/internal/native/nstructures" + "github.com/tech/sendico/pkg/db/template" + "github.com/tech/sendico/pkg/mlogger" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" +) + +type RolesDB interface { + template.DB[*nstructures.RoleAssignment] + Roles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) + RolesForVenue(ctx context.Context, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) + RemoveRole(ctx context.Context, roleRef, organizationRef, accountRef primitive.ObjectID) error + DeleteRole(ctx context.Context, roleRef primitive.ObjectID) error +} + +func NewRolesDB(logger mlogger.Logger, conn *mongo.Database) (RolesDB, error) { + return db.NewRolesDB(logger, conn) +} diff --git a/api/pkg/auth/internal/native/enforcer.go b/api/pkg/auth/internal/native/enforcer.go new file mode 100644 index 0000000..848d4fb --- /dev/null +++ b/api/pkg/auth/internal/native/enforcer.go @@ -0,0 +1,256 @@ +package native + +import ( + "context" + "errors" + + "github.com/tech/sendico/pkg/auth/internal/native/nstructures" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mutil/mzap" + "go.mongodb.org/mongo-driver/bson/primitive" + "go.mongodb.org/mongo-driver/mongo" + "go.uber.org/zap" +) + +type Enforcer struct { + logger mlogger.Logger + pdb PoliciesDB + rdb RolesDB +} + +func NewEnforcer( + logger mlogger.Logger, + db *mongo.Database, +) (*Enforcer, error) { + e := &Enforcer{logger: logger.Named("enforcer")} + + var err error + if e.pdb, err = NewPoliciesDBDB(e.logger, db); err != nil { + e.logger.Warn("Failed to create permission assignments database", zap.Error(err)) + return nil, err + } + + if e.rdb, err = NewRolesDB(e.logger, db); err != nil { + e.logger.Warn("Failed to create role assignments database", zap.Error(err)) + return nil, err + } + + logger.Info("Native enforcer created") + return e, nil +} + +// Enforce checks if a user has the specified action permission on an object within a domain. +func (n *Enforcer) Enforce( + ctx context.Context, + permissionRef, accountRef, organizationRef, objectRef primitive.ObjectID, + action model.Action, +) (bool, error) { + roleAssignments, err := n.rdb.Roles(ctx, accountRef, organizationRef) + if errors.Is(err, merrors.ErrNoData) { + n.logger.Debug("No roles defined for account", mzap.ObjRef("account_ref", accountRef)) + return false, nil + } + if err != nil { + n.logger.Warn("Failed to fetch roles while checking permissions", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), + mzap.ObjRef("object", objectRef), zap.String("action", string(action))) + return false, err + } + if len(roleAssignments) == 0 { + n.logger.Warn("No roles found for account", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return false, merrors.Internal("No roles found for account " + accountRef.Hex()) + } + allowFound := false // Track if any allow is found across roles + + for _, roleAssignment := range roleAssignments { + policies, err := n.pdb.PoliciesForPermissionAction(ctx, roleAssignment.DescriptionRef, permissionRef, action) + if err != nil && !errors.Is(err, merrors.ErrNoData) { + n.logger.Warn("Failed to fetch permissions", zap.Error(err), mzap.ObjRef("account_ref", accountRef), + mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return false, err + } + + for _, permission := range policies { + if permission.Effect.Effect == model.EffectDeny { + n.logger.Debug("Found denying policy", mzap.ObjRef("account", accountRef), + mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + return false, nil // Deny takes precedence immediately + } + + if permission.Effect.Effect == model.EffectAllow { + n.logger.Debug("Allowing policy found", mzap.ObjRef("account", accountRef), + mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + allowFound = true // At least one allow found + } else { + n.logger.Warn("Corrupted policy", mzap.StorableRef(&permission)) + return false, merrors.Internal("Corrupted action effect data for permissions entry " + permission.ID.Hex() + ": " + string(permission.Effect.Effect)) + } + } + } + + // Final decision based on whether any allow was found + if allowFound { + return true, nil // At least one allow and no deny + } + + n.logger.Debug("No allowing policy found", mzap.ObjRef("account", accountRef), + mzap.ObjRef("organization_ref", organizationRef), mzap.ObjRef("permission_ref", permissionRef), + mzap.ObjRef("object_ref", objectRef), zap.String("action", string(action))) + + return false, nil // No allow found, default deny +} + +// EnforceBatch checks a user’s permission for multiple objects at once. +// It returns a map from objectRef -> boolean indicating whether access is granted. +func (n *Enforcer) EnforceBatch( + ctx context.Context, + objectRefs []model.PermissionBoundStorable, + accountRef primitive.ObjectID, + action model.Action, +) (map[primitive.ObjectID]bool, error) { + results := make(map[primitive.ObjectID]bool, len(objectRefs)) + + // Group objectRefs by organizationRef. + objectsByVenue := make(map[primitive.ObjectID][]model.PermissionBoundStorable) + for _, obj := range objectRefs { + organizationRef := obj.GetOrganizationRef() + objectsByVenue[organizationRef] = append(objectsByVenue[organizationRef], obj) + } + + // Process each venue group separately. + for organizationRef, objs := range objectsByVenue { + // 1. Fetch roles once for this account and venue. + roles, err := n.rdb.Roles(ctx, accountRef, organizationRef) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + n.logger.Debug("No roles defined for account", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) + // With no roles, mark all objects in this venue as denied. + for _, obj := range objs { + results[*obj.GetID()] = false + } + // Continue to next venue + continue + } + n.logger.Warn("Failed to fetch roles", zap.Error(err), + mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) + return nil, err + } + + // 2. Extract role description references + var roleRefs []primitive.ObjectID + for _, role := range roles { + roleRefs = append(roleRefs, role.DescriptionRef) + } + + // 3. Fetch all policies for these roles and the given action in one call. + allPolicies, err := n.pdb.PoliciesForRoles(ctx, roleRefs, action) + if err != nil { + n.logger.Warn("Failed to fetch policies", zap.Error(err)) + return nil, err + } + + // 4. Build a lookup map keyed by PermissionRef. + policyMap := make(map[primitive.ObjectID][]nstructures.PolicyAssignment) + for _, policy := range allPolicies { + policyMap[policy.DescriptionRef] = append(policyMap[policy.DescriptionRef], policy) + } + + // 5. Evaluate permissions for each object in this venue group. + for _, obj := range objs { + permRef := obj.GetPermissionRef() + allow := false + if policies, ok := policyMap[permRef]; ok { + for _, policy := range policies { + // Deny takes precedence. + if policy.Effect.Effect == model.EffectDeny { + allow = false + break + } + if policy.Effect.Effect == model.EffectAllow { + allow = true + // Continue checking in case a deny exists among policies. + } else { + // should never get here + return nil, merrors.Internal("Corrupted permissions effect in policy assignment '" + policy.GetID().Hex() + "': " + string(policy.Effect.Effect)) + } + } + } + results[*obj.GetID()] = allow + } + } + + return results, nil +} + +// GetRoles retrieves all roles assigned to the user within the domain. +func (n *Enforcer) GetRoles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]model.Role, error) { + n.logger.Debug("Fetching roles for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) + ra, err := n.rdb.Roles(ctx, accountRef, organizationRef) + if errors.Is(err, merrors.ErrNoData) { + n.logger.Debug("No roles assigned to user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) + return []model.Role{}, nil + } + if err != nil { + n.logger.Warn("Failed to fetch roles", zap.Error(err), mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) + return nil, err + } + + roles := make([]model.Role, len(ra)) + for i, roleAssignement := range ra { + roles[i] = roleAssignement.Role + } + + n.logger.Debug("Fetched roles", zap.Int("roles_count", len(roles))) + return roles, nil +} + +func (n *Enforcer) Reload() error { + n.logger.Info("Policies reloaded") // do nothing actually + return nil +} + +// GetPermissions retrieves all effective policies for the user within the domain. +func (n *Enforcer) GetPermissions(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]model.Role, []model.Permission, error) { + n.logger.Debug("Fetching policies for user", mzap.ObjRef("account_ref", accountRef), mzap.ObjRef("organization_ref", organizationRef)) + + roles, err := n.GetRoles(ctx, accountRef, organizationRef) + if err != nil { + n.logger.Warn("Failed to get roles", zap.Error(err)) + return nil, nil, err + } + + uniquePermissions := make(map[primitive.ObjectID]model.Permission) + for _, role := range roles { + perms, err := n.pdb.PoliciesForRole(ctx, role.DescriptionRef) + if err != nil { + n.logger.Warn("Failed to get policies for role", zap.Error(err), mzap.ObjRef("role_ref", role.DescriptionRef)) + continue + } + n.logger.Debug("Policies fetched for role", mzap.ObjRef("role_ref", role.DescriptionRef), zap.Int("count", len(perms))) + for _, p := range perms { + uniquePermissions[*p.GetID()] = model.Permission{ + RolePolicy: model.RolePolicy{ + Policy: p.Policy, + RoleDescriptionRef: p.RoleRef, + }, + AccountRef: accountRef, + } + } + } + + permissionsSlice := make([]model.Permission, 0, len(uniquePermissions)) + for _, permission := range uniquePermissions { + permissionsSlice = append(permissionsSlice, permission) + } + + n.logger.Debug("Policies fetched successfully", zap.Int("count", len(permissionsSlice))) + return roles, permissionsSlice, nil +} diff --git a/api/pkg/auth/internal/native/enforcer_test.go b/api/pkg/auth/internal/native/enforcer_test.go new file mode 100644 index 0000000..0fea3f2 --- /dev/null +++ b/api/pkg/auth/internal/native/enforcer_test.go @@ -0,0 +1,747 @@ +package native + +import ( + "context" + "errors" + "testing" + + "github.com/tech/sendico/pkg/auth/internal/native/nstructures" + "github.com/tech/sendico/pkg/db/repository/builder" + "github.com/tech/sendico/pkg/merrors" + factory "github.com/tech/sendico/pkg/mlogger/factory" + "github.com/tech/sendico/pkg/model" + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/mock" + "github.com/stretchr/testify/require" + "go.mongodb.org/mongo-driver/bson/primitive" +) + +// Mock implementations for testing +type MockPoliciesDB struct { + mock.Mock +} + +func (m *MockPoliciesDB) PoliciesForPermissionAction(ctx context.Context, roleRef, permissionRef primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) { + args := m.Called(ctx, roleRef, permissionRef, action) + return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1) +} + +func (m *MockPoliciesDB) PoliciesForRole(ctx context.Context, roleRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) { + args := m.Called(ctx, roleRef) + return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1) +} + +func (m *MockPoliciesDB) PoliciesForRoles(ctx context.Context, roleRefs []primitive.ObjectID, action model.Action) ([]nstructures.PolicyAssignment, error) { + args := m.Called(ctx, roleRefs, action) + return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1) +} + +func (m *MockPoliciesDB) Policies(ctx context.Context, object model.PermissionBoundStorable, action model.Action) ([]nstructures.PolicyAssignment, error) { + args := m.Called(ctx, object, action) + return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1) +} + +func (m *MockPoliciesDB) Remove(ctx context.Context, policy *model.RolePolicy) error { + args := m.Called(ctx, policy) + return args.Error(0) +} + +// Template DB methods - implement as needed for testing +func (m *MockPoliciesDB) Create(ctx context.Context, assignment *nstructures.PolicyAssignment) error { + args := m.Called(ctx, assignment) + return args.Error(0) +} + +func (m *MockPoliciesDB) Get(ctx context.Context, id primitive.ObjectID, assignment *nstructures.PolicyAssignment) error { + args := m.Called(ctx, id, assignment) + return args.Error(0) +} + +func (m *MockPoliciesDB) Update(ctx context.Context, assignment *nstructures.PolicyAssignment) error { + args := m.Called(ctx, assignment) + return args.Error(0) +} + +func (m *MockPoliciesDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error { + args := m.Called(ctx, objectRef, patch) + return args.Error(0) +} + +func (m *MockPoliciesDB) Delete(ctx context.Context, id primitive.ObjectID) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +func (m *MockPoliciesDB) DeleteMany(ctx context.Context, query builder.Query) error { + args := m.Called(ctx, query) + return args.Error(0) +} + +func (m *MockPoliciesDB) ListPermissionBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.PolicyAssignment, error) { + args := m.Called(ctx, accountRef, organizationRef) + return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1) +} + +func (m *MockPoliciesDB) ListIDs(ctx context.Context, query interface{}) ([]primitive.ObjectID, error) { + args := m.Called(ctx, query) + return args.Get(0).([]primitive.ObjectID), args.Error(1) +} + +func (m *MockPoliciesDB) FindOne(ctx context.Context, query builder.Query, assignment *nstructures.PolicyAssignment) error { + args := m.Called(ctx, query, assignment) + return args.Error(0) +} + +func (m *MockPoliciesDB) List(ctx context.Context, query builder.Query) ([]nstructures.PolicyAssignment, error) { + args := m.Called(ctx, query) + return args.Get(0).([]nstructures.PolicyAssignment), args.Error(1) +} + +func (m *MockPoliciesDB) Name() string { + return "mock_policies" +} + +func (m *MockPoliciesDB) DeleteCascade(ctx context.Context, id primitive.ObjectID) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +func (m *MockPoliciesDB) InsertMany(ctx context.Context, objects []*nstructures.PolicyAssignment) error { + args := m.Called(ctx, objects) + return args.Error(0) +} + +type MockRolesDB struct { + mock.Mock +} + +func (m *MockRolesDB) Roles(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) { + args := m.Called(ctx, accountRef, organizationRef) + return args.Get(0).([]nstructures.RoleAssignment), args.Error(1) +} + +func (m *MockRolesDB) RolesForVenue(ctx context.Context, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) { + args := m.Called(ctx, organizationRef) + return args.Get(0).([]nstructures.RoleAssignment), args.Error(1) +} + +func (m *MockRolesDB) RemoveRole(ctx context.Context, roleRef, organizationRef, accountRef primitive.ObjectID) error { + args := m.Called(ctx, roleRef, organizationRef, accountRef) + return args.Error(0) +} + +func (m *MockRolesDB) DeleteRole(ctx context.Context, roleRef primitive.ObjectID) error { + args := m.Called(ctx, roleRef) + return args.Error(0) +} + +// Template DB methods - implement as needed for testing +func (m *MockRolesDB) Create(ctx context.Context, assignment *nstructures.RoleAssignment) error { + args := m.Called(ctx, assignment) + return args.Error(0) +} + +func (m *MockRolesDB) Get(ctx context.Context, id primitive.ObjectID, assignment *nstructures.RoleAssignment) error { + args := m.Called(ctx, id, assignment) + return args.Error(0) +} + +func (m *MockRolesDB) Update(ctx context.Context, assignment *nstructures.RoleAssignment) error { + args := m.Called(ctx, assignment) + return args.Error(0) +} + +func (m *MockRolesDB) Patch(ctx context.Context, objectRef primitive.ObjectID, patch builder.Patch) error { + args := m.Called(ctx, objectRef, patch) + return args.Error(0) +} + +func (m *MockRolesDB) Delete(ctx context.Context, id primitive.ObjectID) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +func (m *MockRolesDB) DeleteMany(ctx context.Context, query builder.Query) error { + args := m.Called(ctx, query) + return args.Error(0) +} + +func (m *MockRolesDB) ListPermissionBound(ctx context.Context, accountRef, organizationRef primitive.ObjectID) ([]nstructures.RoleAssignment, error) { + args := m.Called(ctx, accountRef, organizationRef) + return args.Get(0).([]nstructures.RoleAssignment), args.Error(1) +} + +func (m *MockRolesDB) ListIDs(ctx context.Context, query interface{}) ([]primitive.ObjectID, error) { + args := m.Called(ctx, query) + return args.Get(0).([]primitive.ObjectID), args.Error(1) +} + +func (m *MockRolesDB) FindOne(ctx context.Context, query builder.Query, assignment *nstructures.RoleAssignment) error { + args := m.Called(ctx, query, assignment) + return args.Error(0) +} + +func (m *MockRolesDB) List(ctx context.Context, query builder.Query) ([]nstructures.RoleAssignment, error) { + args := m.Called(ctx, query) + return args.Get(0).([]nstructures.RoleAssignment), args.Error(1) +} + +func (m *MockRolesDB) Name() string { + return "mock_roles" +} + +func (m *MockRolesDB) DeleteCascade(ctx context.Context, id primitive.ObjectID) error { + args := m.Called(ctx, id) + return args.Error(0) +} + +func (m *MockRolesDB) InsertMany(ctx context.Context, objects []*nstructures.RoleAssignment) error { + args := m.Called(ctx, objects) + return args.Error(0) +} + +// Test helper functions +func createTestObjectID() primitive.ObjectID { + return primitive.NewObjectID() +} + +func createTestRoleAssignment(roleRef, accountRef, organizationRef primitive.ObjectID) nstructures.RoleAssignment { + return nstructures.RoleAssignment{ + Role: model.Role{ + AccountRef: accountRef, + DescriptionRef: roleRef, + OrganizationRef: organizationRef, + }, + } +} + +func createTestPolicyAssignment(roleRef primitive.ObjectID, action model.Action, effect model.Effect, organizationRef, descriptionRef primitive.ObjectID, objectRef *primitive.ObjectID) nstructures.PolicyAssignment { + return nstructures.PolicyAssignment{ + Policy: model.Policy{ + OrganizationRef: organizationRef, + DescriptionRef: descriptionRef, + ObjectRef: objectRef, + Effect: model.ActionEffect{ + Action: action, + Effect: effect, + }, + }, + RoleRef: roleRef, + } +} + +func createTestEnforcer(pdb PoliciesDB, rdb RolesDB) *Enforcer { + logger := factory.NewLogger(true) + enforcer := &Enforcer{ + logger: logger.Named("test"), + pdb: pdb, + rdb: rdb, + } + return enforcer +} + +func TestEnforcer_Enforce(t *testing.T) { + ctx := context.Background() + + // Test data + accountRef := createTestObjectID() + organizationRef := createTestObjectID() + permissionRef := createTestObjectID() + objectRef := createTestObjectID() + roleRef := createTestObjectID() + + t.Run("Allow_SingleRole_SinglePolicy", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock policy assignment with ALLOW effect + policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef) + mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) + + // Create enforcer + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.NoError(t, err) + assert.True(t, allowed) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("Deny_SingleRole_SinglePolicy", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock policy assignment with DENY effect + policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef) + mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.NoError(t, err) + assert.False(t, allowed) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("DenyTakesPrecedence_MultipleRoles", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + role1Ref := createTestObjectID() + role2Ref := createTestObjectID() + + // Mock multiple role assignments + roleAssignment1 := createTestRoleAssignment(role1Ref, accountRef, organizationRef) + roleAssignment2 := createTestRoleAssignment(role2Ref, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment1, roleAssignment2}, nil) + + // First role has ALLOW policy + allowPolicy := createTestPolicyAssignment(role1Ref, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef) + mockPDB.On("PoliciesForPermissionAction", ctx, role1Ref, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{allowPolicy}, nil) + + // Second role has DENY policy - should take precedence + denyPolicy := createTestPolicyAssignment(role2Ref, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef) + mockPDB.On("PoliciesForPermissionAction", ctx, role2Ref, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{denyPolicy}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify - DENY should take precedence + require.NoError(t, err) + assert.False(t, allowed) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("NoRoles_ReturnsFalse", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock no roles found + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, merrors.ErrNoData) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.NoError(t, err) + assert.False(t, allowed) + mockRDB.AssertExpectations(t) + }) + + t.Run("EmptyRoles_ReturnsError", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock empty roles list (not NoData error) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.Error(t, err) + assert.False(t, allowed) + assert.Contains(t, err.Error(), "No roles found for account") + mockRDB.AssertExpectations(t) + }) + + t.Run("DatabaseError_RolesDB", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock database error + dbError := errors.New("database connection failed") + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, dbError) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.Error(t, err) + assert.False(t, allowed) + assert.Equal(t, dbError, err) + mockRDB.AssertExpectations(t) + }) + + t.Run("DatabaseError_PoliciesDB", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock database error in policies + dbError := errors.New("policies database error") + mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{}, dbError) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.Error(t, err) + assert.False(t, allowed) + assert.Equal(t, dbError, err) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("NoPolicies_ReturnsFalse", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock no policies found + mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{}, merrors.ErrNoData) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.NoError(t, err) + assert.False(t, allowed) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("CorruptedPolicy_ReturnsError", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock corrupted policy with invalid effect + corruptedPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, "invalid_effect", organizationRef, permissionRef, &objectRef) + mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{corruptedPolicy}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify + require.Error(t, err) + assert.False(t, allowed) + assert.Contains(t, err.Error(), "Corrupted action effect data") + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) +} + +// Mock implementation for PermissionBoundStorable +type MockPermissionBoundStorable struct { + id primitive.ObjectID + permissionRef primitive.ObjectID + organizationRef primitive.ObjectID +} + +func (m *MockPermissionBoundStorable) GetID() *primitive.ObjectID { + return &m.id +} + +func (m *MockPermissionBoundStorable) GetPermissionRef() primitive.ObjectID { + return m.permissionRef +} + +func (m *MockPermissionBoundStorable) GetOrganizationRef() primitive.ObjectID { + return m.organizationRef +} + +func (m *MockPermissionBoundStorable) Collection() string { + return "test_objects" +} + +func (m *MockPermissionBoundStorable) SetID(objID primitive.ObjectID) { + m.id = objID +} + +func (m *MockPermissionBoundStorable) Update() { + // Do nothing for mock +} + +func (m *MockPermissionBoundStorable) SetPermissionRef(permissionRef primitive.ObjectID) { + m.permissionRef = permissionRef +} + +func (m *MockPermissionBoundStorable) SetOrganizationRef(organizationRef primitive.ObjectID) { + m.organizationRef = organizationRef +} + +func (m *MockPermissionBoundStorable) IsArchived() bool { + return false // Default to not archived for testing +} + +func (m *MockPermissionBoundStorable) SetArchived(archived bool) { + // No-op for testing +} + +func TestEnforcer_EnforceBatch(t *testing.T) { + ctx := context.Background() + + // Test data + accountRef := createTestObjectID() + organizationRef := createTestObjectID() + permissionRef := createTestObjectID() + roleRef := createTestObjectID() + + // Create test objects + object1 := &MockPermissionBoundStorable{ + id: createTestObjectID(), + permissionRef: permissionRef, + organizationRef: organizationRef, + } + object2 := &MockPermissionBoundStorable{ + id: createTestObjectID(), + permissionRef: permissionRef, + organizationRef: organizationRef, + } + + t.Run("BatchEnforce_MultipleObjects_SameVenue", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock policy assignment with ALLOW effect + policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, nil) + mockPDB.On("PoliciesForRoles", ctx, []primitive.ObjectID{roleRef}, model.ActionRead).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute batch enforcement + objects := []model.PermissionBoundStorable{object1, object2} + results, err := enforcer.EnforceBatch(ctx, objects, accountRef, model.ActionRead) + + // Verify + require.NoError(t, err) + assert.Len(t, results, 2) + assert.True(t, results[object1.id]) + assert.True(t, results[object2.id]) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("BatchEnforce_NoRoles_AllObjectsDenied", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock no roles found + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, merrors.ErrNoData) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute batch enforcement + objects := []model.PermissionBoundStorable{object1, object2} + results, err := enforcer.EnforceBatch(ctx, objects, accountRef, model.ActionRead) + + // Verify + require.NoError(t, err) + assert.Len(t, results, 2) + assert.False(t, results[object1.id]) + assert.False(t, results[object2.id]) + mockRDB.AssertExpectations(t) + }) + + t.Run("BatchEnforce_DatabaseError", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock database error + dbError := errors.New("database connection failed") + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, dbError) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute batch enforcement + objects := []model.PermissionBoundStorable{object1, object2} + results, err := enforcer.EnforceBatch(ctx, objects, accountRef, model.ActionRead) + + // Verify + require.Error(t, err) + assert.Nil(t, results) + assert.Equal(t, dbError, err) + mockRDB.AssertExpectations(t) + }) +} + +func TestEnforcer_GetRoles(t *testing.T) { + ctx := context.Background() + + // Test data + accountRef := createTestObjectID() + organizationRef := createTestObjectID() + roleRef := createTestObjectID() + + t.Run("GetRoles_Success", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + roles, err := enforcer.GetRoles(ctx, accountRef, organizationRef) + + // Verify + require.NoError(t, err) + assert.Len(t, roles, 1) + assert.Equal(t, roleRef, roles[0].DescriptionRef) + mockRDB.AssertExpectations(t) + }) + + t.Run("GetRoles_NoRoles", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock no roles found + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, merrors.ErrNoData) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + roles, err := enforcer.GetRoles(ctx, accountRef, organizationRef) + + // Verify + require.NoError(t, err) + assert.Len(t, roles, 0) + mockRDB.AssertExpectations(t) + }) +} + +func TestEnforcer_GetPermissions(t *testing.T) { + ctx := context.Background() + + // Test data + accountRef := createTestObjectID() + organizationRef := createTestObjectID() + roleRef := createTestObjectID() + + t.Run("GetPermissions_Success", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock policy assignment + policyAssignment := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, createTestObjectID(), nil) + mockPDB.On("PoliciesForRole", ctx, roleRef).Return([]nstructures.PolicyAssignment{policyAssignment}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + roles, permissions, err := enforcer.GetPermissions(ctx, accountRef, organizationRef) + + // Verify + require.NoError(t, err) + assert.Len(t, roles, 1) + assert.Len(t, permissions, 1) + assert.Equal(t, accountRef, permissions[0].AccountRef) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) +} + +// Security-focused test scenarios +func TestEnforcer_SecurityScenarios(t *testing.T) { + ctx := context.Background() + + // Test data + accountRef := createTestObjectID() + organizationRef := createTestObjectID() + permissionRef := createTestObjectID() + objectRef := createTestObjectID() + roleRef := createTestObjectID() + + t.Run("Security_DenyAlwaysWins", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock role assignment + roleAssignment := createTestRoleAssignment(roleRef, accountRef, organizationRef) + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{roleAssignment}, nil) + + // Mock multiple policies: both ALLOW and DENY + allowPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectAllow, organizationRef, permissionRef, &objectRef) + denyPolicy := createTestPolicyAssignment(roleRef, model.ActionRead, model.EffectDeny, organizationRef, permissionRef, &objectRef) + mockPDB.On("PoliciesForPermissionAction", ctx, roleRef, permissionRef, model.ActionRead).Return([]nstructures.PolicyAssignment{allowPolicy, denyPolicy}, nil) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify - DENY should always win + require.NoError(t, err) + assert.False(t, allowed) + mockRDB.AssertExpectations(t) + mockPDB.AssertExpectations(t) + }) + + t.Run("Security_InvalidObjectID", func(t *testing.T) { + mockPDB := &MockPoliciesDB{} + mockRDB := &MockRolesDB{} + + // Mock database error for invalid ObjectID + dbError := errors.New("invalid ObjectID") + mockRDB.On("Roles", ctx, accountRef, organizationRef).Return([]nstructures.RoleAssignment{}, dbError) + + enforcer := createTestEnforcer(mockPDB, mockRDB) + + // Execute with invalid ObjectID + allowed, err := enforcer.Enforce(ctx, permissionRef, accountRef, organizationRef, objectRef, model.ActionRead) + + // Verify - should fail securely + require.Error(t, err) + assert.False(t, allowed) + mockRDB.AssertExpectations(t) + }) +} + +// Note: This test provides comprehensive coverage of the native enforcer including: +// 1. Basic enforcement logic with deny-takes-precedence +// 2. Batch operations for performance +// 3. Role and permission retrieval +// 4. Security scenarios and edge cases +// 5. Error handling and database failures +// 6. All critical security paths are tested diff --git a/api/pkg/auth/internal/native/manager.go b/api/pkg/auth/internal/native/manager.go new file mode 100644 index 0000000..7bcb25e --- /dev/null +++ b/api/pkg/auth/internal/native/manager.go @@ -0,0 +1,51 @@ +package native + +import ( + "context" + + "github.com/tech/sendico/pkg/auth/management" + "github.com/tech/sendico/pkg/db/policy" + "github.com/tech/sendico/pkg/db/role" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "go.uber.org/zap" +) + +// NativeManager implements the auth.Manager interface by aggregating Role and Permission managers. +type NativeManager struct { + logger mlogger.Logger + roleManager management.Role + permManager management.Permission +} + +// NewManager creates a new CasbinManager with specified domains and role-domain mappings. +func NewManager( + l mlogger.Logger, + pdb policy.DB, + rdb role.DB, + enforcer *Enforcer, +) (*NativeManager, error) { + logger := l.Named("manager") + + var pdesc model.PolicyDescription + if err := pdb.GetBuiltInPolicy(context.Background(), "roles", &pdesc); err != nil { + logger.Warn("Failed to fetch roles permission reference", zap.Error(err)) + return nil, err + } + + return &NativeManager{ + logger: logger, + roleManager: NewRoleManager(logger, enforcer, pdesc.ID, rdb), + permManager: NewPermissionManager(logger, enforcer), + }, nil +} + +// Permission returns the Permission manager. +func (m *NativeManager) Permission() management.Permission { + return m.permManager +} + +// Role returns the Role manager. +func (m *NativeManager) Role() management.Role { + return m.roleManager +} diff --git a/api/pkg/auth/internal/native/native.test b/api/pkg/auth/internal/native/native.test new file mode 100755 index 0000000000000000000000000000000000000000..6e856f07d1f3fdd89547decf08abea5e72498f7d GIT binary patch literal 14400930 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

<62bz@>@f#h1+5Qyi-6(eSr_ z>d^3a2f+8)pk=QQ%pj)z_K;yonvKoF2V6wF);x8p7+YuN2PJDWd#)O8D*A2>_x8OQ zUflPMP;cK2;l=8!3qI&}>3dvk$?jJ_Wwp$hUpTp&`71^`_T025R>22-V~Ou$_k0c5 z=y%&NQ!y48oT2p)1zKr$rO!!n0?HU&L;=*IA14@lntn$D7czO$HkPySK9}n%D+>WP8^9c;&iw zV9ldn9fE#}-Hu&En<~Er8Y+ec4q_`8nDFtYtkl{|t=hcB@h>;;YFgJZ z&+-5F+wD2)Ja5KQ?X}$+O)SS|YG++QZMI_c zia0ra!56J+yBqrU=2?dPe%^$d3m8XJjx)bb)|R9LDuGWabBix>#*A!5*9BW&aOdU7 zi<^5TSDl!1xb==#m@{SS-Sb@g?CO8}cUEf&@zUwO-&5@0nfCos-|u4k_o&tq;(oa~ zAA%kOtdV_gcW16Sv2{XrOIK!ecMY+>r*E^?-uVsY&Ac4C8QJpMcdfP5JCR%d!In1S zqLO#%btPN>nY#Z^THRk#SG=LvN86XIogG_DQRjBYW_j)>%#V0n+m}kXALV|uC>vh) zicUX1#p)qeVud1&sx`<1o!+aKyLkN#fcMW_CpFG6`ts{@jsFCGD0IEYFosXQ-S?!w?|)9~?+b2!@_GFIS@<1Ze_Ah1 zxA&5EgKN84=zqVC_4K}R|70(}@|Hh_-rv4@FWNu+-`Kuj>7UbkG2ZnT8E>9x+#BRT zr!6=Dj#)=slFy;*5?x~t=5t^VEI>Z%`QBU(*7Fv`uy-6DPubBPy^pi$CAA(@dVS-m zFTd&0NdH*&?|$s?&ROuE$NYmF^xtET2inf(kUe6LXJ~Y9K8H7++#KkEqmuaIK;t16 zxu$B%=ZZ$mVl6fQh9W;~MKT*pZ~BQ<_30Ohw-!~F1b>qLJm$ttJo>WEZCK?m!}SsW zwg0#3q+i49c6=ZBCCBTvt@ArQ_u%`;`^AqROxX&^{5U$a9UZ&bm}er_vF_aXk(S?= zo3*yx!n*HY?L!t`9Zf_+=mzx7D13@X6z_Tc!58ZuZ#q>lWA__P1xMc~f4t+3rlkkh z6Nf*(B2?BqDyyvL2=-g;xzZQw&JAg4Iydu0bmuej4vYxI7&4POk=t9BKEM72VqhzO{J9sq&{2oSMiZx(hSzs4ePR7d z{C(-%eI~0XADz@rT)cz$_zL;z^q1d=45m+Pw&VFtrl{&V*$3BRAB1fCpy~x^s}R`G zpJHq&yL_svpFE%AzjnQGaY64gbaE^hZdFXZ7@7U5{4~}hJM*K7r?Q@3*9pwGG)9ZG z9xT5}V>^{$)`~XkSoZ`rH+GCb4;Mxgl8-NvKN1NtPxs!o#dW=7GHH)zlKb+lR99=J z=>8jpQ~5M6xb<|M$68Asbs||NQII2BAbp;WZn*Q8{dJoBtsyQyN|)8qr=LDO`rB!* zFO5vnU%~KbQJ6VyFyRFy(3A~xXs(SMgm!F_AUxU*Olo=?UTkB{Hif4^>wi9v4aeN^ zJrkYGS-A4-cENKHx-*Y@T0fHhJ+!#Gw=xv&2@Ek6g|97rQLv~77CVfY&;$&|0J|*W zZ)2H%Mtx!LH1N|$`MgUUxOrulcx4y6WfvFr9!A#4*3KoC{~Ubs3iGdFUc|RsYM`~6 zCj0lv8fdNUa>*U^FzZ_$J~lCiS{D!1j5n7tRDpvU8wc29;HHf{igxU2`Z4~zE8 zfnP3sqx^%F#C39sw`i`#@XsdrrxTb2!ExK-F}+)D8_?=mjf|a28{14qb0&Rxa5(*T z--J@ekOv%2FP>||;SJ`Yex}&{gpYfDl-l!k`XGkXTGHSn#GT8C`$~eBhd`~ZCkCm7#%i2f9O6YMaKHEN%u^<=U zF-E@~tT&FnhINglW_=fYyOs5`QQ&Di^fi@9oUvJ=FkqW)Y1N&>&fGx%=5!S>m?tW+DWjUz|f(DNSMMnt#K!7ZD})B5Lc zGk)Ys9J$hlo*9U4W8he_rwqEURSv=`Q&a}-4;u4~?LSaAr2Ix;ta4Mp*A#q|a_C|M zbkHWfMwxcn_tPu>4%{-wu4H^vXsWsDJU3 ze=Pf4Uh`z(6nHIM@WQ&O%;AN_b9(KufrAdlCw+V-Y}Q(DXHN9y46olP{`ZgV6B--* ztTH9;*h|;(8w&4u=xx8iNBD)k9UHH zu7hUxVQidLCi7T^mBAWZ;jt!@v3eBz)3o1uT>TX0s5<>{PV0SR>8wIpVI%o>_%tvteU=S;Li@^^s+2#z2KPZ_Nkbg$Dif()xDB` z4$i0b3g@DShk%L3b%;679c1*<=$fk-TWetG z#143)TXe@5pV(#Dww3IoW!OjJ&kpETF~+4YIkuH|ZzcH-qtVfg_4&PV7q(}hA+_Vg zPJ*rXxpPg*qRU)a#P~f~w2iUMV=VIm!zXq!mJY^Jg-p0RZ7jAvaL4jD?pVBgWxyz( zu|(^0d%GBe@-3xOIr_8K6E7F;4Y55{TJpbd+2ujZ`mQWzUiZi{g{WA2|ny~rI;+{a&M?EJ#>{T@!@@b$kSn`}D+d}^$UwWMs~ zsGVE)A#&?HSgw$rFF8e7t>>KjcYHanbAyI>2($ofBZJ|2o=c|ZBd-Ks!8-_L4%}Uz$IV;6)P?&N9}Rf&&B^af*$=e4M>bWEv)|SqNH_iGv(Ojt_H_I} zx=D22PcP*l=U9*O?5hjPeBf!v@Zc56{1d=ce6GIQx#!nY4|AW98{jkQO^@)Z01xnsT(}o)ybKC`(LfKjkM{&)p=&H8Bcu6XS=#O zl5f&`lM9)1YNQjS^tm;93w~X_;QnXtki^U zT~dl%%>o{h&nsPBvYcn~MMamA;meV`^2rL|0r{Yv(DGDI2E$8-rB9H-O-nP^OWw&= z9EHsGj#b!Uo=(|;9d;T%xR6fS|0eoaG|Je?Lpxapjf(%u;I)I~zKkGFBiSK(Zx?TX zNBOqiZ^`RSXn!e?n>G!@b0()GDUahr8Xt$x`tegbZz)zR z8?(s6J3h-pF5W%S~S zqWlx(C{BO}LY)0{`Xg*0j}IcWu|JImGU>y9?(#q+jR(LZYbJr7LD+|3WY1RCrbQdX z0(v6&3hmI2Y##CFKS=I@2U`|Elgc4coa48^V-&Pt4MV2Uo;A!QUgDnglwkMjj~t%a za{+!ilV+bw2N0_ykHyhbTE|RCN70{0x9#X4oBqHlete>gzBQLJ`gY)x^0PFCqwW~m zePi(O=jf-De6)F#Ij?ki)Uo#h>vy>J-gfN0F4x|Z?IWCcw%#FZy=~xKw$KsyFzZr| zx8T90S?fD+eqkAPsCIr{m)Wc;}D9NAG)v4_BszJX|2tHh~9^ADq1k|JV58{|fwH z_+}bE_~7sH!b3JMWUTjSLFaDhUc4dQ=EWXwBVR%>vD4U&ic_}Nk51y|z@N%1E^hwm zhmOotjBK6DBOZQc*y{(wlm2SkEsWt-VhhMjt;KiT>c~m)dM9#0ar_?;=c<$5W-hX~ zyMhnyo{Zd*Jtv#jj*-;{TI{$Evd`ARlot$2sUupIEiPFn8BgwCb1gBZVr+7PXBBgZ zF=e%<(qc$#yK{B63hG_Z%SKR{L4cKZej7`s_+I;beu`FAspMfU?C+{c-J9E?8 z)l=A`h0H~=tOHr5@%v?&6-MvVZZ7;^&A4lPv5ct~)cgHo6tYXC_vim0CC|j$^MSV~ z&&a)pzEbgif1JO)%qogqi%(~+YuWWNW81P8_=%U56tVZONHRk*s112ma{aP(#b)T) z)2wI2(82l8m)03O@SEA|WUtRGCl^q#kpHdtZ|pKJ=hd+n8^;lQX&l+IX*~HU`wLrc z*^}l2u5fZjZuI6e1YD3m`~%;7=G*?j0P}gmEGhkH*^7|~{taW3Z7th&_SYQSR&q~k zO`^RT@Ut2G>?8LA*}$G5Cr(gBOrY(F=#rX{S<*q>@{pOZA!6>SMMn@^FUfJmC<9}O57KVwP;0$pxFO@Vt{j@f&~-OsKE%;!LaQZnr0WWqQxNbOHdDIQh@g}5U-SEE3oE}lwnrS(*s&Bvb#zxi?r7KT^pF!-E7o0M! zK}kQn4Zf5w-T`bop>6S``hEDVzCGQ@rLpj*^hghKZ2@v^0dkFmuSC{^gV$%}rr@r2 zbI6~&7T@;c#IA0hU*G#0a_oG6Jf~H9x)Fa|xo79aHtEZYZJxKzBLA)5I*S`e0z~?E%-X0prJj;j1?p_FP62 zv#m>x{m{xeKGV7u9KX-T?aTw# zvA=|8_H*#_N+NjA0r+_On|;rmCTH?Y@RHV_G3Q@{W8!}{4YbmSbZ{KK-S#PH;C5)B zQgd`@pc{HQ4Q-r3hW0=g8t<_;`}VvFEnf>w{Dv{z!We$b7=BBizlA2AesIY8r_*R+ zm-W8p4?z=u2~C`se^>A8lhSEIxsF3^nlPdX#?vqEKZ)@%u3g9(J*%PbIybhmd1%14 zHDu#y?kmZU_Tv64(P{Gg6(e@|-+5oa5n>akfjRc(68YT35tqo`5sY+i#UL|*R~6c} zO#Xwx^_|SKmiFhuvxUrKJ2C&xGU(z4{Cna*f8T1YMdsQ2$d@U0bOUgNpCro~$Y+rJ z^X~PF`7;;6Tk&=$=CAd`Q0wR1c_#Tn{4gxT9*4&l+p(+a8OthS{vC`%mz$o`xrtGc2CWfG5YogW2$4*657&r{T?P%bDvq_-W_ceRix%IU}-1#z5Eh zIuLxS800`PhHNWruK~?;VuvT@qw{v6^8&zGHc9sUIlbfpB!crt_u9S#G}Qs^E1v0J zFQh$ChJi-^5OoN3e7uiXCBm-?}QANuno?e6 zFrK+FM<+&t?%d_t9BXnL$F_W!H7kuV9lyiV@cZZm@H?NJ@PX|0wrTLuEMSP;Zp&h1 ztS5^H!nf8TyMVPH@AH|@tH`k#4`#2IkCX)s47Re5EwBb1&$7lH%d!UBayJW__;b+^ zFjAQ_BgVF7S=sipQ3o8I`Z=DT0Y+E5@^mb9lQ!-&a5f8E!aKH&4lcWawfe3{uZsT0 zL1#U*uXoktKyQyCN2M6v_!4?= z)nCU-p1v{oo>k0yNjqbcFRi?}I(Sy{!Irl!9Vhd`&=sA?55>T3+koH7b(ji` zje*AY)fd>bRfP-;;p1VC?-33E8}#Mb6iFF2lVJ{B8kv^`a56MmS z5u$&|wjFM}$~Aya$?3{%C7x&Fq!>FVirp-~(TkCL@$o41t(+3pjh1LG{xZ4jrA*QD zv|jnRP`mjpuWZBuY-AVbxiH!9gN4L)5+1fc zk;*?;p0xC(w@&*_@*3oeqI(lsgVy^|O35!M0mqik%(d8g4^`#8K98EO*$>is6}F8Bt>Q_G8yUzdr$ z5^>s;FH?hF*Ishb&uZG%Z?!L6YRq|KdB+hk{0Q%Jap38!EBk-jv;`MqnrJ zPFvFca<2Vwc)|yV3(?QGBI$!vK9XR7Z@j0DHK9Y#fEpHAM8XY(CE?@AtpTsl@PQo484QnS-ve$=_p&QaIUhsH`jD>?ST!^rxI zi~@2`$Tw!sH#t$kX7Vggl2^RC3HUd$rnSFEC;?TIc! z1@y=7I^=`oefS!OE>07($QVlQ5;B;5{i`=;@y-C`cLumUMEqteHctk9AL6=}^Ol^9 z_3CF1ys(w|9;F}IIANYEhb0L8D<3Gx^_=>e$x{N8Cn>JBjsB(*M>`C?)U!Uc6M9kp zPdz!ZnT)HOdG&?N*EfM9?Gr0zFXnSW;#|mh{=2>jg2z|f@oT(-(^ufZ)3*O;wW@E~Oq-zj zT9fC;%ynW<9d8ZjTgbtd4!Rk5SaE^<^E^s&(916yYCuEHS z)|?NG%n}}V&11a8b|-v_{-nz@fkPqV%>a++jAv{*U=H2EF|CsIiZP5PC!#j%!52@% z&%xY->$F$1){L?B?>wFdfSH~hHs%={Z}40#unPhs#ff#Vix`6J>O+i8`2rfd+Sj3S z=%$|b-o6Uye6=NB?j}D%x?v9WUm#B=o3Xai?$yK^?u4FhhMumTe@E{i<~lQsUJRSB zmq6dL{kpNU;=#el%8b?>>=P1gX)Ul0UJ%S`wH67!L$s~6`X2;r=uF7dGM(-z(-1ca!+NQB^lE(5ZtNCVVEN^~w zZ=5j-E^+qPLm$uBG)G^eM~m?d5-%|BT*i~jcybvJ^m<%toE}V7&QG81&A_U#75Zys@A@G4;J9#VnR`Dfl|7nh!&-5_-WBV!K=wWi1 zGIFAcqa4MSIeQm4YDX_CcS$*9ZP2#X1F(rEDQ=st%c_ujitlLNHS}{KoyL0ZDb+`w zv91to{Sol5qP*Z20N#qZ10auOvMs?*@U61dBrt#;)?0dk=_L|Nnl^AbI}cEiAO;V$Z5 zOfGnPefO~^HHPi-A>Nhe*%Quw_*6U>TYbsO$qneiq4yjCrYqRr@CdPFM^`yI`jWJ3 zzs}yKK9sXJ1eyl6#{Kj9d&Kt7Q|o)NY4G4I?5|tUN671rmrcA6u^f}g> z8Xv#r5A4H7m&fp%ynF4hM2p7g{?+i~<Lz1avT=TnarLn+ea{o?cn|hG|?Tg)@$8Nbj6&9*|A0NZsYV8bkFblo(wUz zPTG4ai6dvt4m!2pXBNN)@1BILK=0L))3*g0C}Rz*9@^5_vaQVKR_JRcJaDGIzPAl} z-vK?zm7`xc{&{2r~nNUnJ>5^wl%JKl%e3;E~cZ2X>|Z!G`3 z4*w#+ScBl%!ELDZn~Aq=eR zR)X6aa4UOXcK`mxquGn<EPx+554P`~dOXVe+%3+qCz?fJ^K_ zm)9I0PyD*VrjKCH4)&nN!A}|Vv6A*`hgqy48R)^GCA(i_U=3>uoxpw@aℑp9;?B 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

=)Aq#!)ae0gq@!u_G1ewi>5*_S$*Yz|2^F54il} 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%0D<5juyduHI0y`b z(GLeN2E!;H9mc#4^@D@qXu&>Wc$neC!_)!r@EePVtU=-->w|;gXuU+o;GOt07HC+4h%i3!} zIi!l60oLm@O^JgTYh zA-|?Qt&{^)%)2;;`qs+JYahJ@*~^P`Zzk73zUSy|3^654lYU+boR`3iZi8RQFw&0!y` zx$*t6RMV^du^P7oYE>h=oO~Q>+8Q`7tD%{36flP7spQ^$RZ6aV(_7{ zY0g0KQ$u>)V#YY;-u&YlpXU7+GuEy2U!9&*vzi?L+Hj;{P1tET-P1FQ+S7(LsZPTw zuGJ2;&l4AuKQem`kT+58#O6G`J$f}q9FBexUKdY!b)3IgNPdM}a}n(*PwV|te&*Bx zcm3HviGO?dHyOkH8nigJcaC%F$;X&et$^RE$O(;e-YRg$*DhSS){X)k~O=4SXC-b&=}0+-%T@NSOF-=;nW8tjL@Ijo1ShQF%~e}@f!i@ss)4Y$zG z-=W$J@%KW(OFyiSYq{d)`u6Nw>T@o0>c?e5*E{J$d@g!2=PCI7V;*xE`BvtZG+wXn zN%K9bMbjCc)56Zk4;nAu9RM%qn6H-He-L>2)z9B2UZ{^A4Q{fp1b^YkD#^nX?M>_3 z|3x;Tw6M_Blr5TVzsu$k$APyo{XKiD`|JYu*@gDmk0m^NaeOmZ*AE}Q=Bz`fq}_>!~wCbnhV%sE`Q)Q`Q= zsUM#csBeQW$HJGAr~SxM9WvCs!YMk)-_`7E@O*l>rVU=b4}BuO9*BSc6u5e0UjK4i z^e4d8R(@t{}bRU^d52b zTz(?1?s-?Z8hbT0HTWiRzifVAzpS0ug!55yQ?VPtG@ZA{`at#p(B3!Nf1v=MyaT_? z?&Fl!$0pu20N(zSdHBTmqYeK`eQ_2B^^F1Q8?Ak8s=`LjH|P9yXS2tH=a&}-Y+NE6 z`Gov(pWkNv@=)!LBd$K94XpuGJ6=Dmd7IdheRF;1q+9A2XFB!E$6H@cKDz1$<(F%Z zpv}{qqUW)XW&Bo<8mw8A9;}&(&D8pwLe_LWzp~6vq#H8eCUJt&|}#L0e@x3QfToav}m)m*r6Cv>twaB;4PLGrVW1| zYOf92w1>hIJI3A2UN5)QAIfp+U%EO_-+>Qv2z+YX8nec_Xaegap~qGyvQxfHdumF} z!Ss}x7ods34>V!j!$if6J;YR^$qTe0+Rq|>-Kcs8{72PwDn8*%hmqT8Z#He6Coc17 zr~L&z+Wq)k&xRK01Z`H;IahqWmAc3a>g>VWUvajC-+%Smn#&%rUR&E~OMD<6csFhS zBuLy`5#jlKw~o=qQeN9>oOA58z5ZRo`7zq`!#M_?n&rkQ_~P9y3zSQ+@uYC%#VU7! zpNsp0gNoq{Z8jk^_Ep=Fk{$ zqjO>U!_5`orkVUkj5ClpGds8K4rCo&x@l$UnTeuHS$=LSa^$b$Hon#!k%s0n=u7?~ zo%2O>K7sU!S-;Ek=w1^isLoEYyz&TI%k>mBTs9A)oKH%hIJ;xE)erLNjeOCDYR7H9 zg!7XY5Iay;numO#XN#I2&fr~6jn^UC(mp?_m!#FG{xKyf(%>XH4Zo+Ylr*l>oQB_;cC{x@ zYV8U3F16&O)ZvLl({7;LU|EWZ0 z*&J9X+Qyhgmpx`&cI>;&b&A2mPTzQcXs)$BI~;x5T!*7k_xnlrdz<_Hg!{eO{ocs8 zIX?v+)EalgU+lf0^NCs*qj*f?(w_Ro@YXu;IF9&U`?yGVSFtX#nC}kiLz@d(uaA7R zmBU+iI}I)6PQw^-0x?Hx-a|(Yo~wA|U}|vPRwuazowUl1m2;obT`h$Xav08Tk4Jev zsx`?iclYHXE zXD`F^!$!ty)Jumu*DV=_d@gmm+re)evfIMBRUMpN6hk+)f^YT|YiL4WrNMLRYXZJq zXP$fy^3D}Q>U*%`L%25&zwc)5appqpE%<%fi!ewnyY_!=Wc`U z*U>|6ZZ59Z7ZzWr*PGD~>{_M?pkh8Q12`@qfPj0)|?RASZ-$OmEHufkyM)rav1-8TGvs=W*Pg~!+jXrlHy zdn50r-gAxcRdn8o@FJWTT(D=uP2ff68r;PGhWY5ES?D6wcWRG=;qcjQTuYa0ZwuMb zO~`!-a-S!;M+f82n7yO6vY#FGywkTK^U4!*&Y$$`v<`6Vox5!LGu726H=j}6yQgF9 zzCqraRqvj1lk&sl7xsPcDd^G2UWTIKlUy5~OToqs4NhSHz=oDPStEEwctZOmr=jCt z>{`Ja#fvG>VrT)+K({+tFL=dFo`G)v63P7=WJx(?n@_g;+s$P^vBde1*1}M(cu+R8 z3EPO+{?Iv+em=ZadZtBu^c)+xs0kYdJ)eQqr*%vDki;jH&zctJ_A7ISVpPP}nYW|kbTaC|)?U}Y3 z{!n}phF4a@GcRzBKfe>3biLMDiuc5a($Rk2tMqsen9isaty@=Bavsw}^Bn*9(z8rNry_+6|S z&1HX=2KKh>%PW(`D=FwK=FEtl2I7@P;uZL6AYM6iBfJ9b6M5wm{qf2I_+)YdpUAJe z6+Q`FoKLo6_gkhW@X3KdZCHHbw!t1O4Q78j+OqwKM`pt#b9#A%814P=i1d(jjAVvA z)~=IIinx3+mpu1&?t8w-8u&uK2DiTcImktqZ4$TxiYMC<)Jfz7`kN(<|38ppvORRQKmorbTnE}?N8z6AW=g5GSc z%Fi|Hp{GY0etq6s8)?_>mhr5G*Immy0BmUk!1n6___yBd-`JObTOJ9lLf4VEXD*0% zUHBJ1%zW6SzC7GElzE5)0W%*qV9alL+4_4{x0|`K^@{mF(wCRldolk#f7_4VLpR0B z@b@3dU)3k(7cVMrGHJk=e|n#Z!gw=jycZ*JcHgf{cp$rp2fJbLXOFgjDv>zne#5@>|cEBqBitH zrc>GAeD%DU%NxygjIol_d06KJ2Y99h`099GF_M7~*;bsAjC}NWZ}F!i$DfD)%Qy#= z7^tW?)j72qzRaQRapB5_9w%JR{B2PSwIa_uPOkP;)ib5^rDvGexju*I+S#uxCAjgo zbxw+TK8Jb+J+Jd0i-$X>0&bhnyU*8g&bDbIwX&h^YG;LD&q?pwPO0)xZaaPFcD(sm zE1RBNM!}ttOK+XoHrLCyY9Xn#GR}9+n?C%zhLC?^0=;6{wxgbQn1&lX77cJ zXclW^vSv=HeJ)t}YU^drj;;Sbw6=|PRGnN;_(-VsBy#$>UwLb(W}h)W4RjP769otU zMvG6@WG|#g^qj8s&s$$$^+WIas^Ap%35jfcRkac~gD-Dih>@yE7zK}Cfloo~ZS{a% zI~B>&nwsn7j~~t2H@WuXGkS3(n37!F^tXOW=S2DSQ|Qy=3R9fj!RuHj=V))2$j;z( zD}?t67{^2Xn;O+WCM z;w$aX5~dyT=%RarkE>1AJB;+6Q-7?*+Z7_exF-F%$FxrODf*JG)ZP)}S^p{-Vve-| zp1D4T&Qx1pr>-RvS<>I|xtT*R`LttlqFM*B1iH44k{!*6D(~UfpBmd(@LC3}Wx%R+ zlMex>|CvPG9{d#a9pQAZURiQRG1w2#$=Z)5Lfj}ARMVq%OBsy$h}OpPOcOe!6@K`) z1?M)r!pGxZ(dM67TPnW&rv!e9^M|R;>D{kFYi+exMFFxbTFF*K%sQ?tVuw)l8{Z*+ zGz2+ECp4kAvZ0&ue{~UOg>nUT(69u&*3Gf6yA}3P2$^Oe3n1lHAC>0=-|=7?qw2+Rt^^^JiL14qBFwnD&QlQ>0TtB zqmLzyQ{#;aO^<;eVC;ON2Rn!2*Wt?|Z zCsT4mf{Y`R!r$f4$S=Q=@hoIKgFY`R+s>5Qf6HYI2G3Tu{|%fpgWo=~jjSiiw$|c$ zxsRxehkg8JHNE}c!$+FI#{lxo*m~tT!^rb}|DgQ@eDaKp#gJ2z=S6Ofn*JPgFh@=N*jZIWJK`(LQ+AIMwXc5b@Okj#inh@I6Z8*0%S++w zC-8T+;^*weuM#}c9nw3J{dMK|b@Z=#BKcTpoQavuSju=OgZE>sofiyR$L{%b`VPB) zD+bcv+3fY`wecYP1!U1iCT)D(-#&VIF1a?-J}~;*_u4@pPt(|^(5`HS#+EOA2z(X7 zFXPesi+O)7d{f3aE1abGxD>N%FZm*Wzu;7RsGELy-rw(lt8Dhr4I}v|-jH6Tb zMR*!$93f|#fthC~u!o4AE#+Cm12)GnF#VCq3+T@~qQCO4iS!S!PFZp{Up^VM5JsYrvmZ1B!h;T3wk^`>O&scDL(k9?Xk9cpk7M$MROh;dc;Z^#gAcn8AnVz@w}0j6Gs`rG%kzG|Z30HY-O2Nwt@(xT z`nb79KCs0{eb+ztJ@b;et}xHM=(~QNYp?xdT>E{TC0dh@j}!9gL{A@zKhF>A^~Z(3 zz3|-;_T?20w=>p5E0>ENS)7+9IejOzBF02i=nA(ZznobXYh8B18xJ4C!t##@15km zF4`y|AGhfAx0nw}kH)ZT#$MweR-t=hyjyjpjTJ>(Wb^tMZOPBw!~WK~&W6rwflX`f zt40D#Rb@kC@XU0Bdwjlp+EI;mKJDZ?fm%PGY^M$JNg?lxPx5&`pX*GAJk`+rRr&CH zBRpbx#^NiMho1poH{si=wiSP^s1zO)eM@hMbO)G=Z8q^|P<|+Ji0nitI$kz{cT1V$ zzCwFMFdpKG>-DVm+k41`MSsua8QFBL(_XCWVZcSqyok>Q^n-u>@J;i0jrW^IK-Wn& z9z66pcrFFFM^y4#WEK8RzxMb+v)@qfes#tdA!n!YjRcmvLe8$!*uym+w&U>5WtMHd zl`)J4pFy6Jyq1j7_=jV+M@FX{IBW7cO}Enj73|B2y$mv*$9drGH$PMVcA0Z*8L(>~ z6Zsg@Gv52B_Tj^W@6q^51?ZB|S2H%Xqj?2>k72_Kv0u_{h0sjjG4#0hU{!8i{*Kyg za-YwSkhg}eh2k-IqY&OGgf~PNQjFPW^emQn=Ql**avh=mFE&f=8==xLL z7cECsHt4g@_7T$zXP22NBeY6*ioyoBSn~jVgk#F#})wS*DH6VAo37`y=b8*PFeu zl+y@c3&oFKe6M+b`3ipzj&-fQM#AVF&67SimVHCmw;(3|j_U*Kn+g6@Y)C45)NOK7 zj_gHeg*a1Fuy}A>F8ubw@m5yLCkBRNwuPfeaB%%@;Yh=FQTKFg!m4FwCSkX9Mn?$U z>A{4r*$1Z6dj}8GkKCJJud}&m85M2|ROiz&Go~$j?-qV)c@h%|dv_+duUNuC7%wUU8u6RhZj9^>fBf%yaBEd~AH_+!Ic4MRre5k?_LYT2T^y zU;rJw)d^7dl4R>%(gW*mbCPQc_-%8NvpeoHp;P=m(-!9Jn&I(e_-P=U8AC4%zGnFP zG`b_Y*Y7hm$;RyI=)W_i-)-;^6rK5Hg& zaTYl;ZPU(wBs}NUENljTk3Z!DeZu9%jXqj9{PXOq&RO^8H-!77KDbYg<9;l--@fv; zGwtBM$A|l6_^8s|$u9m682r0+N8n#|N5cOi`Qq@NVw6^JjX!^EBeinU57Ni-Llkec zg5N&08rKi4umz7(J24QgR*F_?6L~+4{!XOTkHAx$R^0c~Dp$1P-M+LUeomm(-EkUN z{UJVG44rB{I?={g6Q1$tbVCB2o@c$K;(^CpI(hUtZ07EJ*M7T#Bbw=C>uB{=irO7=tYd814B>VD*tjWLhq1&4Pn$5%Z+tatuf zbKb?|qa1XeYB^f6spTjJZhn{SOULa}T$o2J?)pZWvrfi0l27$>;DbK>`bN>c$jt(5 z3>1TDbOf-SU4$erUrlKmPWNU(Ef*@J|K$x)po1 z1poTBO6%8X9(+4Iw*@%I0_Rfnk>;UeGkC`2oUHBAnr(|yI|s`g0%s~FcYXU(=BdeL zjFYZHFHwJ*`yg{M_&Vhq;gx0RspaT7)jr)$+g|Nc-&z*-R?pUXaVfPQ5ueOJC)pU_ zXng)3_(bDvv@sU=ln*m=^YDwo8{;x_^XP$i{gaDZe6H^C_}}o)(dCS{p7AcFy>YZR z7oTea?ajvDIs#uT1J>ohe3I{>)aHGe{$A&k7Z}<#8hJX29(a8=<3uMmlDCnMrgOYv z)arR-X!#%1o1zO(!oRQ2CI%1p1LYT+6srzbKwljjm|<0=N>N z1<284$u58+)tE#^(~p~@*}Km2qWVaJr-du=#!~V$?L05vXEbfAUH#p`GvjG{+)$@G zRoD1Et?+CLACLcH%C(E9UE16UeyiaP%_VMvH%ftT5qK^G-_z0Ksx4d1_}2KwwiQ~P zW6oN%ui|;-s_>z@^N>mDKk^;jt&DAxJGNLMd_fM{8{317D^`eX(YATc9h-7cLHv-{ zX*-Ly*YLaYHaU!Ev#uG>YR2*eANfV9L+hQdbL$Yix`Qn?zWxMjY2$SVTOzq9eg5aE zs*uqy6CQGkPFE!z`~DyE>g$3|t+9(gLC2s^yW|Hxi|ic$hy2#O7kvZI=*$6cEkg&s zWGA1Kd^l{58q$rz)vdH3n}Xr|r<86JF|KzxCIsXKLe|#SZpe z7p`7IZ#8{~{*mROlboVw!0ChcaZW1o`~zau6LX;zdj1D#*jLV>`9TtAcn2cg<{r7% zecXEk7~bOZCZE^&h!1&g)ek=s>E3ZU=d67^()|j#flL2K(|MvzHv{4 zJ|gVZtat5soQ=<-qxYKiTt<(mUYGdbx-Y;#(j${rEiHC{k6RtuNF9c*rR(m)r;xo_D?bdlTfuiTeKx@h(v|JtTq*-q%lb9~y!k(spt>7jL-@X_^-{MRPMVRu{pXr=vK=pK`2zMsBb9+4h4>(k(o z24It&HaZHpJUS`Q{9Sy3#=C)$cG^Ve`OgpkG%NP$dzPa)cdK9!(-30o-1OIY7wqi84VjO?B!{_88Ynuy2 zgOo1jONtudQ?;*etuyH(|I|f!u&FI`&LHqoCvD{0%gy{Pu*p6QLB5rn2_WBoy^*Lh zWLG;3J}jS0XGs74Gq%9`$|3Z5@7gr!bJ>@zz}|trXnM-woWAs~t23F0EO)wF(S7Y3 zobI-zPB-UT7j2%;nZLy36Vc7ZX|@hV_CT~OMpt9sizcQa|LE!oN#HD?IV}5ZJ~+{{ z*!11fxtH=z=OfaQs&h??e*P+A-lyPi@D(dZR=}lvv9_g*VFS7azVCd*>XehN9KRHH zrcu+LTZ}DOP5WKUn^hz^rz*R9ii$6FR!{+6RQwTV1y!O&#g}6*&hR~pdL!)2$sFd! z#`An&NY`rlR`>$N0p@Jya1Fh00`_^pUg5TV)8*W!?TvrvDXO6D5`EKlroL&r@%KGN z!llM|g85e2kaqM2Iku6V+nsJ;HRpntVl#AB>zbKPQ3^KWMr@tVs9XdLZCgB>;hj&} zYcswI&%|+pe06TOxbV)X6zz?j0Dm0`d~}5R2g&Wkl{UvWcIEgp)xdQU-iTo@s1N9x zlZPz}By~Oc8K=$kKTY6CD->c zrVg&(m)?Mei zVNNU$dCAl_^3wP=-^j~GzO_D7yeC;vdw0^ucKWD89-5Goz5G^?Lme}}%|h?Uu1UA* zS>@qdkt^$Cnf20rbnIr~>Q?jzINr;9?DrCFMP@qSd95ea*yAz-u3vOzMlsr9e5>=+ z3djaez^9W^b6Luwwu{XmXF<@Lo zU4^NuW-jRfcxYsNt&C4~U~R~~bZWD3!nL>0muC~dlzz!8M2A4XHPnm}|Dod+Lp$~&la3R7 z2LH;_*!3|UjMoFB*6Oy2PK?WwWj~$Zk3O;VqrfNK=Fw>^u!&bSw)^Q@eBBDYR12tn zH6Nh$wjtq)doA3P-=uYyvv^**TRHa>aBcFRsdlb01$udZTiqUx>g)#fk;=2u;Tm6S znbX}7A7if`e?D9F87Z~ra^cG|#t3aY7-t{-4c%N>-r@NGUu1mJF%w-MU_3s6&bqZY zv}=s~G1pVTqw%)mclID#L%Mp59Bsyai!XcN<7VXU6-Y0K)Y<$?G&HRr9b7(nheiD&th!1XsGc)!;|oCULf3cduLBk z=WM52yq(WjevnFSz--Ici@~3CuHxu?as(|Ej^W?T(VS~Rj^Nq*ICI2(wxt5T;MvF7 z^KYAtr+3Ilz5i^_b(g!ma6i8*_n>%syRMNloinNWFvVj(;W+~{*L%$M5cB(PbM3DG z^*!@L-}Tex+O|{AbqO_2o*upCH1lQi@HvGOU^KqWQt$yT zq4UXXzKip!f!D4H4oBCSacED#^yuBb>$}Xg-H&dCxz;+!^ypW)_K)?qzhkWPBi*yT zu@alG9<{q=uRB(7ZRA_yui-cDD`x3I)}oJTE{C`od1;@o_5MrG6p(}IGd9|esZHR2 z+#8$wn>RMqQkc4SWc^FPr#X}&19M8W!2MQTLLS#A$w9nM?%@=9h&RYTycxPQn#4Yo z*Cso)d4Ws2UWeC}gT-Gfx^vZ!_WYjD-r$IGNApfW=hP+GuVS9dMmM|(-@MNIZy*cG z6UZJ|-Io?Uz*^%b?rA;uwFU;^I2gT(>kQ2&x_KMrg!i+~(#zR=*IlFR%?HdiYoU7Q z1Dd9~wWrj}Z(^=Mb+?N@@uXd~O836Zi%XflE^(MqF2XZ$R@9J5+ap|8QMCa9>Va@?*$>cVCCT)^8_> z`&J_x8ndS_V%d(#DS9x9@1nq|y8l?Vg>#duE4=+KKIFFTulag{XS|xP&A_R4K1#oW zHAZdbEMm{+u|LF1$KrWY=bH~_J4rSB(C^BdsunM0JUX7fH}m{9#!8;(nC8K^U}H8B zH_Gm49|!+DK?U`LW?cA9#neN0Fvc8e0W-+~EOCNIs`==wxoUE?vZY)0RXtYCTK^;` zT+>3lrMX^P&zBT^6JFDr3(aGUri}{bO~-?4YCWi1)moa0ouk3IY=|EQ)l^r57jKV= z2f3~^f2$UgwHW5w>W167j`Oqp82I@TkDrHe1|zcR@XzDt3Dg(+&kOR_kBkxT!PBM8 zPijwe^YdB3e4dx07OT~CBQ%KHsLAIMHA zw<(|4%MF!+x2rB?u0H7O_VD*r^+C>5_%eM_@0r2-W*=ty3)nhNug~kE^dpL#(lQ*`gcjs8x{9DnDMdmv3<8C~~j#+ky>KJkdBmVqi%|&!(Uo zPvD2lBNkQ7i}+kR@c5B)8)7`?n`htQ)u*mxT=KI@-T7Y4vn$@vTxX0iiH`4fJk7wR zT$W-KlRL#{GJ9l!tJQz$?2^sbejzRFKVouG4bF-usRyXol^C@{ssy4JaDS`+mP<7i)a*hFnXj*Ev<@W7mCyms_8 zwI3L2@Zj%b1AY9Cy>LBV-8aqIEj+&4_wN^sD*`K?91CpJt)fSZD!vj}QJKj%zi*nz zngQt7HpJrfDb|IkgJ!l%iny;la= zUOw%W^4`8dzBg6yGgjs%jW1NqSR?Ew^4w+4(Snus%(hX?yEY>?G2~}*5Zj6^lg&9T zn=>z-qg=1JaW=IQ?DNyThWZTotv;J`)U!Fo&th{VAK+Os8$&)@kP+<{d=fmLp2yrx zf*pDUJs~@!nu=lYUSy18+PiW$dX%>M*r9}W)n+St!RQ3wZDWu3ZRp(;_=Nn&D#_m@ z@ID(p>Dd=pQ37A|MBoc(vjsgYS+w%2^)W-D3(}lNw~lpoBPYAGheizC2(C@&(aqLY zte36WjNcsr57KX&kwvriM{`daJ38w6-p9vXqsFDE}IXXxQbZEFnu!IDng|8C&8 z>LPHAxCk6i0f({Gz#PDL3dnx}9@lq-26kP@<~5e4@=b_4)=R%rr*eG@&#K-anf|lE zgZ?%?Q;N+e$hVaLR0X}me3}@i~ymt7u8#ND^t ze5+_xGQ_7p)ei6Y@q8!GR@B?MmzJRwJ7wpj`>@$XEyK*2kt3yN^7VJ<-kq`?tb*sz1F}DBu|HYJ z^UI9W*c|raIL!56bmgbj=Id6+RGbQ|*pFRF4>K=BpIMBdSHDFD)Nhk$L-~$R4(p`{ z>ys{^$LNdD<72d;xv#bb4*rUKf+5i;xtEziXS#U2bXeu9AJ+T8g&o6A1^d{k)z~S; zR@uyzcy?4d0MRl;+gbzsB(ZrO-(Tjt)H(Akb+27Rxt5(%?5H(c+WV?w0Xe}L@RI(TMg#f2uA<{>nJ2AVR$u%Iu%{xx0o7U^~-({JF-|up}IECyLi|Bpx$le9qr@# zZhfx7jPw7tXN@1ge7|xHG2-c_JYpx8R^NhFt9xm+K3;QA-fBi_wKj6w^rlf`h(!TOyYEhIK9!x*t_@nuZI7#`7}=l)?0h+!}(LlGjbu{ zw`n@<-U?kfAKdwn^XQiIJ-ftd_)g(F+v(mmkN(og zSB`SJRjZrLUNL3t6~mhBu2S^;8upASV9%JD-?nS|bGTp4{R-wVHnOI~O;QQHU z*(V{FXE&xe(>$JdVdvQmr6J1`Sz6aMeyZh(5q)`lHFmy<{D5)@vY#p93;q^gWMb#_ zyYevEL#>YT^b3`xI$LT|ycz)Q&AwT!EcJfhlb z1P(hMJI`nFX2-Jyc&KNz^{&9vOx{*|qWGWb#fN>rkv{w0_x5Hxc5ZwMtsy}ax_h}SH;?0>O$CS_**&ydgSxaI7ZGVHV(QNKL{UYM8(cg z-RC!ChR-~U?yYSmPa@r2IG&oJams;)x~ho78z<4`WaI~XB;J$nw&>iNbus*D#ifli z$VH|T3y>#mMBa#Pcg65?lyCcxVq3mn0*1KX!{7SN;(LzePugmt|2t`?lKopenLO~L zvl}+`+PWQC>(i|s;Mqi5C;wi4$$9qF*0q0KG4WZ5y)X(*oEnP9 zsnzhmeC0NHPUjdY&S*pa*vB3J+tt&^km_!mfJM07LO#5;m^#j9o!mC$(avGAzY=4P z4RLm7bT}(CkCO2OwH3%!#x8t+z9mPM(1`iiRk2Vu(^{1ei{35pRK^p56|d=?B7AK4 zf|`O=Rn9#-^^W90`}66UQtYw47m44~JZi=+-+O27dDwgJ=eqYYd`SQHy>G_fJIFna zNim1UHJPyscEzq)?zP&91kU^~=1z0rhiPh~HI!p&dj0Qa@_Z}z@)<)Dc2sMPgd^EG z<&&Oz3%>yys&z(sPChj{VpW`n!9(wwKub@5`!;uI`OVfSczeUzXA6yw73$hT-bpzI z_-=>%;W6-CzPp|`U-oC_G|P8WE#IBZ_pK{-`L3jYzUyaQu?r`BG6LJth>xw9B9rU= z)WK=&iFvi<;6Xk`0kM3LyqfG<@H)Q1e{2%JGmd<8qGVI+iahz`_Zq?K)uyU`U3JyD zf|YB@NYWHLrx2bZo`k0T;MBv*i5-39aeZ7K=~MF9A6`B-XuO!UgI%{nlf}zQ+iP-q{{0E$Lyab1 zlY?EW+vlt}K%8CI!X9IMOTVaIU%zkSJ116ieC~ zXOGV>d``BXHB+wsiaeafT;p@-X%D6{WLNziW^O#p{A32c+7{qb-uNSw^ zKno9_+85rBPx+mHjgKwg>6g?RRAT3Jy&rv1_olZN)5Q-ypltE4)OHGWqO|Lg%xixF z>*oFYHIT#2J;8dt3CfR-v~#(m`3)PrV!UX^{O(4^tiSg&$0&Ju4E&hAlQj1yJ&2rU zv8FC>JT*tyXyH%1Ci_&ZHhD&W2QGD{|DArjj5cZO>s+7UnpmOe4z3%q*V>O}6Z%_c z>FvdTN@FkSB=>qQ&uQPPt@M@5_d&j?Jt^Nr`^ysf9nh(3_wJlZ<0H@7I+_G%9%%(1z}ZMe z7~lQ{?y1e^Y4h(c($+`awl?v67WCRkTT_Gw;aC2p=m8D_=mQT2%lTd75Zrk1-DCM2 zpfBWSmGYZ^eUbkD$lP`&de7hANcz(}lGk1~?U_BnXkWD`myqjh0~hj zS!>?f-1hdn@b3eD>)Y71+@5c~o^g8D-}YUv^IhNXyRPP1a~=U`_8NR!z@CHJk4iNK zL(pT*(iil(cIDRa&BiL;Wr>Y;zt%B_1U_+5MUy}|pIuA%5xO&_Wm4Moej z_H>-&WSA?1xjy+jjQ^83->u0eqeJ1XnfOPGsST`_jtd7@lrs0BIrk;}w#}uV zcz(P1LvaJwqOasgGFGB*_sy?dxtQ2mMe_2%rC(XZ_566KlcS25@;H|M;V zd~=~bPKPFB3(%{~8$ngh*Aps&o{Nc3T9zx>Y}7Ll`f zvCGZLWA~ru+UgSzr#3eBe6_oNKu(u=#Q<%ix8BM+I^6oOjBzx2Elc{&_1WC|g6n=r zpIP)NyvP^Y$i6rZ`CNZ&?6v>J_t5@^chUY|CeZ&R>tIFy{?=P-KaRAVzV=%-O}U8! zqg8_=zsHN)A`fTc_iQ9L-%LEK=cEHa#++K=rNm#~3#@nu8PA!2on zUwa0qtt8so>$a8Twv|L%@!#CGjDMJ2*-)8!X8IC7_&%&<^XGZ|w$tWm{P^(rUcQ(A z_Y7q6qqZTQ=?|v#4L> zTJ|r=$n-nIARYV_4d$hj=+M}~}XDCmHEqJf|RCT6jdds-V zhO*(*KC+)hnEfos6PI5@Z1%76S%9JBzj}5x@=Sm98i*bTIX_%64`(tRX{U~YdjE&L zm?g_esmZ`z4uM~riGk!_s^+%`xh_KndXVchd?wkX{`<7GWY>WsW2aOH?B@LTvR={T z*oU#-%K-7=Tx@w_J~xIgdJaFs*p;uLJAmU*e4jiwhwIAq8~C=CXYB8Kw*`F$pR8Bz z;tx;vnNL}tb;A^^_u(yf2+{GA%cZ~T66AsyUHXPvPjJ<+$`RW+z)9)`AB^6-BO zZJUorI7fPNVK=op;3nJDQ)s_J#i2XCadtykwpDpc+O+c7fBQ+;>12GR6#O+)FGn*F4OBebNv%*Dn z-_)D4gL?NfZPM80VdIoL08iuK-<~g99nX5&+#coP2EduE3-tC2@7=$&^;YmU-?poI z8}%WdzLoKqJte5k$t1@vyluvpD5$jcIkAef8#2?#9}g#gJR2V!y%ocLHI3ta$2n3x z9QzAxRPUT~u6kVyylQAN)kl-U2>heDhH!KqG-*L!wenjv{lOE=pPPKJ;q5$ldy{`& zXh*W;ZDO-T-fpK2>A-Kye0zhgwZ5Knez5t{fxUEI3y*I_A1i;`tB);iRAVFBYyUq( z`@9JGb!a~ym~V|RF0GSFjWUn)sOI}xkTI|J($d|KIS6-W6=N+Bdvp)x&Hr~X_N4;423TUC&dq3|f*58-O zhdQS$=8_dhWdmyrI@tXxSO*JEP5u{odx&TdiiVc zj4TrG`|437f1w(+B|l8L{W7QeG5j&`AB--iW@$@~_4W4AXAAtf7C)kn*e*o9;$Cz` z*%<93u6ew$TLWM5a67!*=5WTKXw0?dU^Y_|Q~TTV8#Z2 zKaVXugkH;_E-8!gRAb9~b!+6|PIx62N;{Iw*}IZ)@s4~{eYc`}gcre~IfXi4TTL6Z zw`#(nZ@8jFa3%VWcLtL4Q{m|ZS z;b_(>YddB#uP+@TomRTn_Z;ISKZK*dk&L4k{*!Ob}2L1iX#TZZ1Ywpubt~d8`fnK|BzT8`{wYW@ciAKOBw`@s*JoY_&W@x7h;<4*jtMc{P;HqKXnjPGLCyvToPF2+v^lgmV&RI{VJ zay$B>1K*+(+MF(=Zk61ie=W?{8T;GRAjh~j9A1mK`|6ynCnlsFe+{zNeq!5_8THx8 zJiv1DQ)$Tga%zw($Z2=t6E7#PeLJ#74(h1(B|3sUPG&4xV^U6@S^E+R?%T<4ZzSd$ zw`}PRJuAsqAdfd|2X6fpT3NDm(#~wV(GboeZNf$ILJ%o!VZjyh``l_o&AuSJlchn&avMe(aa2rr(#i@37tIwihmQrX}-R#csRrNjh-9=1iIX0`p2I7;hzcme({d$MacS zpTL%WoAImmKcDX|?p1V>g9Z<_-bFS-<4Qx8heSWb-aKQd>&*OP&u-!0&>LEJpaT^n zXz!Q)-un&jC5>=)mz=ThHSta}yb|L*b2cLHaSrwd;$zcp{608fzP)=f_p*mOyG@(` zoTk2K7c~XC2IjqY*_s{kg?I`drXh=XU-n&a#@u#3PdlMYoZYe6_S_8$5Q=84alsnZt`Q3uS&7>Q@4Prqmnkpco!4#esb0p*+iZbHR4?Pf_ThnG!xjz%+b!=I zHrDMUAJ(QN!hNxO7mi6XdpkNo^iVwKr%5w3(VpMviXEQq&H-l47c4>EHQ%ueJyF75 zrxO($A&Z6ViTteWII>wv+uHY3?|btHBY9u3!6M$*{DIbE868PJDy@xNH?(R3&z`OH zk5O^WKHlHUrx9HtoAWZ)aeFb$!EeF;zrnQ?!y?@)2guLVn|P}C{5<8on&5GA;X9m2 zPVLyQQ;RW*xgp}hh(qlKv0`tr}xq+MxK~BW0l6Fc^~he*Z*9TPwIQ#Q!6x(ZrTbi_EKASGdLBlYnUS)>m(f! zzLXDs5!|J&+-C5OEi6g{?-Rj$g)^l5YI1<D$yGqo>iR_&YPsaL6DJomHTE;ci?a?G9(brp-l zPJ_Q4?WGkqG)RsHJsNm<#u4EOqQw(_9F3Sc;im)OXF2l&{{5o0?{h^su;OZ9Tr&Rq zua4o|4C2J{rx?#u%$ND$sN*^IyNN$n!E;YTZ@sVlsm9k5`o_-h^Pa}!wU?b*`KsDf zO(S`OhAR5Wrk_W6E;ggGVKIN_@He@-2G#UXAK9nht>C>Sud+ckKFTfBIiYgJ+-gfa zw3_D|@ezz(%0^Gn=AqAVo`>{9Hns$PqnOA)7RJrH%JEn|__f3~rdKvRo6trp?Pf5J zEOW#zAFCAkXo_q}!{zV@!`~^Pv^9F#wX_~7q`hlO^$OZTp?nMJHS113|;2WF@ z$MWwcKsVOt)h;tQPU)H}z06wErRazg=mP4$ibS{e$&zvQsKw{g`N}2_2;D@(_Q{nE zC&6Vaa~ny_ZJdz&AZO-zcfRA5c%I**oA#{4TmV|L@O>QFprS&8Ku|n)L zw(B;D7r%LpkF$t za?YQVR%7A?qsfunyo8T?&MM z_(}HDqlxJsdieXl#NR*kcoKTlLl5?PGBjCDpR{ZGf6ef)*0`rcuM#d@e0M^FarE6u zU+vINI@r`i^S37h$d@Yvg2~SLu_sR-{eRL)tqE%k#81lCy@vfAV*17|W5YYJ19tpF zqUVW|y#C4aH*~-Yf->YFw6jIpMCu=%gfkzP09ETep*f4fU9YL zwdbJ=_9}F>F?K6mxEpTPxBp) z{)hQaj{d@Yhob+^cih&v{`cJ;-tfP7xp?zr$djKmqpy4YZ`GOQoT=bk-awaZ5q0<7 zW8eDb@d&?dTefXaML5`1&zKANZS%6tdy1)d{0if2A`Z}=*5cbW;EQ^lF8QjO2i{E0 zT!rIw&E?*vl^gN@g5^!y?K%s^4w?_K~>O?l0aoM`L?{lr+9yh;f53x}6d;N@e zhHt!w62==~T$$(ckE_PZ&p&gay}#a7dr>uh;H8ssjc5EC z+u>zTnz3oDFEKXZN8`-r{&UMV?Rjq5_&qPK%-nN$S=OGH;2r-sl?&Ebmbl|9bH7W? zIORixqF?G~oB`iBUrCUM<;Fkndmf%@Omx18*4vtS1Z<`DgwR|D@#d~Z>}nHn*N9i& z-mn>7rmmyB6q@FdkJ-q#;xA~{#oE}S4B8^rKB_#x^sVFp$TJ0O&C~j<#%t~TAkVw8 z_IfW5pfv^3gI}kh3Yt8C6RlLN7q5Hr?6QsjExx?pG;bM-$_cS({XD7%={wzRnea(^TNYDE*|!S zhah|QGtaW4?CzQ5a9CRe9ld#X$$;)Hx^rfCi08sr-g`WTB@r%h;ome3I6BH7hR#?5eL6uRK71e%eK-{TP)*%_1KIvm4`nRxlIe z4is-Lr{>x}-x|Wtku0;X&qT%eL2AWRS5Sr?tx6BoNZ&6p*GV<_T)EmKMzyXbz(X$4 z)OwU*XZ7v@^!#7){1chZv1;&8$Nd&+#@4Q1``A|g-g~7zV_kN9FS4*nI3(Ak@%QwE z>J>|zpsu!AXa1tcq|5A>y?V<~RP8CI5-i*3=OFD0j{E4tT+>Gue0LjtoS={W;QKcG zf%}+C<{KWUY}kfO5<{0WM^qFW0sn4sY9o#3&AIgjjK$P3ffKDK$q-J^+p1$sN_A@1 zQkz2#V5IhSQl1UG9nl&{*}@LSBiJh3G5BE@y`Bocz2V1vl1)3wHRKv|{kROUKlE1m zmt9i7!sjR9$!qy2?wjm>U*qwqOY>t>EzPyZi{t+D_-z7bllb!+UL9$%^1zJ0nEb*< z;4Z$*IaQ+XOwN=LUBLZ9#yJxmuj}i%9*u4*2B%rzbP;%z-HFgf2>c#kePk0p6S-92 z4@O7cqIj0`DGMtbt`ffDw6rqky+4%q7jYht+EQ$h_?$zo-Qv-{_fCKR;pX0c!J*kV zvDk6S+2`egw(GE=@*A~x-8Rm*#`c;xX)9yij=fpBY`bzR<&%JMAGV2hj19ic*xcmu zXMlGjzstYUzP^eZM7xW@D}DQ~s+%F^()EwI=3E`t9|ybEf!6}yS!~)5m)Fp5*z_Cj zn!vrqE1v+Ku<@q?=OQ)O-$yRiS0b*geGEpwPoL`halSL$T4kLnDxLQkmzSDGR5skv z4?n$={`%}7^jCZ6?G1n6gh*xM?ndo(hu zysm1j6c@ITqpkvf%9A%$NvDRR72x?MYLZmDT!FtiIvbfq2eC$`dog;hAU#~ue5;)+ zlg#C-hVql=H{>x7{}R4&EAxEpCu8;`AA9b;b=xmamj^D}E0`OhE%fy+ z?QtvlI14P~iq~r|V{)3;JxCpg=2`We_A+j##!_q18w;K8X7pAEdMYgtI5Oc9r}iXp z7CUKO&(V%*<;+>6%pG`V2hB9+EniiennP|*am;+)pTWEgx=Ck2H1ljHxq_1msj;J; zOY1?DZ+-wB5+lxU1l|ySgVv#iY(4dQZ@pdXLK|QIE%54Ec9=CeX3b0zK1w5Tn{;0* zxZMWa?cloy8OjG%)hLCoa*mcYeu7$2<+T4_+TI2}s_M@FzcZ6bCLsu{Sg@!=5+Eev zLxloa+h&r0JZW*)?7CZaH-UhF(RHo6KcQ$7Ajre`G}@Le?7tugm{D76b%k|*1wlcK zR*}~3Zg-btCXWOkXuI2tV)K80&b>Fe83RIJub;0M-kUr3p7TB5=kq<^^F4}Ph2wwz z(wWVA-@MzKi{2mF*iuVdOBoyQwckVT58s6rUTw|-=qw1t1fy&L%57xJEyQn$!6tY& zQDdb{A7ihHsm@n2>F;~@PkGmtjrl#Yzk;{xR`6ZM{Eo5aF+UR=W)CcIm!c1g@y7x| z;hz1&&{F?$pwT918K(BFXo=l7v~=qte*xT#1uuF}XCt6~Q(D{6Ij_>SInZ(*=V#{{ zIu5bvSO_fhpb2zw&rfpc_`L$?!rWhLsajh*1>SUKl5&@HUiK;ISkIYE_IV)G1r;5M zvGviBZw8~|0c5#bwYSsgs2Bmhxmo+#838BY<_Tm*xgV-f{4MmHc)b-qS}R@$Uuy^9 z^`C+ZQ?K2@(E|>z@8x-f{;m4UVhiy-^R|zRg)aScbNaKuAkTh3+P2-AbC$v@!k=O? z(sju%@f+t>vTxDED^IT-X7t;f7j-_;hynE)$KxxoPwMnj=)UG@et68E+ClDLd3_+Z zfITkNf$)3izkRa3(pmrbrQdIEJ`OJ;Pl@Zxwa(M`Ta4d6sTm%ttFszE+38%9&b45K zd7i+}N?c4Is%Q30Ml3PRpGw?=4jsewIM-2JSsO?cn0KBl-w=o;!oYQvg6(Z!yX&*C-ELqT=D_A$lg_okhB3jugv4)rDQc}fT5gSqB|h|- z^Jo&EaD5xte#cd?1w63z`cf@JfGta~4eNvLUpae;nyY=dnJd_ufDN5w@aBQL!oYTs z1DkVAI@bal#sv8ZiQ~T1PV7R>e~yeL-}a@FwUJcvS6rL8{u0=dKH>Gb@>3&Yi4(xp z?MpRx@Vw@tKDg$l!8J~B9RaSTpM~pk1J_UoF6Wwbt_3!H3HIV8e&X~y{kmA zlG_K% }YdcC_QTQAOp@7toj)NXQo4{sP5YhUk6wHM@)_ZE&}!p4-DzK2p;>E% zFZCpR-f;7E_%XgzYYqL*<9dkeEUw_W^%mzH_^EY1cv=9SZsj-HZ*yeuDEirp^G6NN zo&I;)SA+A(4*grf`8Bk4J#9@B&a?Y)PX5h+w(d7$2{~hN+LDYGn6^GDzZMw{)7F)= zRY+UOP+wa)v}J1ktQ}!$s~TH0rYyBXwg^1SzgDa?jD6-TWA=!$cI~l4vPlnMhwAV< zWS^(e-mloJ=I-^qn*6GQ0OvUJp7#3IV4s_zZDNcsr8(kKc%st^Mwinr>mv5q3HL&? zUt&jImVM5Ko;o|tITzQQ<#|Qy8F-=XUi_GsvDMgUos*_oX)V8`J-%yy30pMEml{h9 zS2biNq5D-wz7={H8}yQGg9K}zzcs2XB|Gf{<~Hu-Z@D%|b{bxM$FV_=$_6bR_37qw z;cG5u0bMA*60DJ5D85>Lh236tZhTb>U#kYe*RO1R30K0Ii?3$yy~w)>V%FUz`_fy%#;Z7Cl>s z{^wm+|A*jN>58-RKKeCIZ1?kcpBUEw{rVnj?-$DZEgwS5M+WEpKNl?@N#p$wr8nRc zXej=7dH;{9iUGhIiB_uJv;Blv%X?6dB`XY9Dgmts!;Xvc=kSjSz+TFnsV zLgu`s@W#i15pdF$S^nfN_JNHe|Ydk?ruMkD;*KQC?Rw)J)w zcsdF1YF;Y6J@Gv8B;Ng$b^F11H(hVnfT#3le>2FlzjW}~b{0Qt(@!p(|9%dPpT~dc zepbsT@ZMJIoPO4yivP9_!hfF(&VTQstIwDJ_6&m8!T4{egD>gM*RY4}@E_}ZCPt$8 zNN&8hawj|@`yY1VM(Obp#m{92-T24``u?`;Doz5Te7oEo3OBcpxszw^YC`0uX=!P8)V!=aJ3oSqF&Z*ISEJbmpC z!P8EQxE{J2Id%Pa7f(OE0zB;mPl2)lzWdqm^tB7X)5YhBrgIh z<m3 z#s%Q(n)AfhcRvSTIjb%lU&GG>UxUeG+fVSxF2-m4Y}~=%>tW&RCxhVY;=%EC(FNcu zN$$W|iGsb8Z~_OOxz4_WX}OyZ*d!%)oquYUh8i>4WYAZ~pZk*4W=Er_(j=Y@pcjC+DzL9-Dz-IPc5Q{T$ ztbZooIFhZp(rK_oI7fzDvTZZ0yCckj7tOYgm4g%3-|v2%d79cU;GfnreT+FVinTD> zOcrR~$y_ol9TkrMg@d23Ydi)os~#)hT4SwxjBjh7pl$y++Tx$u%G2Ky`I~?G`w8Kb zel!;CRdC0$5!lklvQ|0iI-4MEEb0A%&sx)u)2?#D+;+3MPjB~5+TE@_z0k!=zx006 z|He1#ZYGx`{Tsz+{}W!<`?~*k&;5Iz`{Ueuzg14O;*rYVN)`ym_BcK|zuA%CTk1KRc>19rY7 z=PW~;#KjIzF*d=ed_=RKXc(~+#>qG~D?ceuI2mT+!hc}RmLz!UtI2S&{akhY`tuTJ z^k_}CnK_r%!SV!a7WFV4SiSHT4U&)g7XA9wCvY*}F}g|l3>RNUJY^a&e#R6=KUHa5 z@UvuFIda>;VLthZLF)BPK&~53-FLhY`Q1%i1mFA}cQ4ozy?V(Xn&{H@EfRCC!SKhJ*}aBfwgM!$!g$jzS4%HwNT$h$QcTD z*Mb+pZu6PF568{Zm<%6i94?-IS#0BJA9!-}d*-lT&$~ZHYtQ6wl}NWq#xfYY@@h9y z*JM27&UD6|HORPYoN zOyBL$y_3Gv_sDdj1DiQp9{ia4Pn=s7YX$VB@Yy7|?U#BbV^Y)6WSih|pc;4Rj%gGBR=H&-B zRp@q|n{zd_}FjV@^)mUkNwuZdMhSbacb^;IZQ2m#HQhZL_|FE@CgY zU>!KdDc@&od5o+19(x>Z3*bTeklb6WKN^}sALO7?K1a(DYh~3}sQ(JR@}UEExzFBW zAN%^hUSs2|B!k~0XTdLh1)<%2!Y}vd!tVh5h|jz*H31WR_IWUUK`?QilYyz}+%VmG zfiR7S4*Qjp30|Ph>D64lFz<&q?=U>hUIWJL(&_Z~2d9(Yp_6lF)|6|2NBB`L5%z(a zHa4BK{=0+rs&2GvQmYPV%Y40qURW!-+h3L0Vy$G# z*S!hfj{QEV=6kV8)Y@#WVSh0DLYixBe$*K#rPv=I`mX&Jfo9(LM#(U;BzEiBJJ)1ejEWQEO*N`!t$@ZGPK4t9f z63#$rqfO=O9b&Ay*Ls5Vz4C@$gO1I$z|9ymcNFXgi1p57JU2i`_5j6Z@vhe9{OGa7 zD#0#2t~@uphPMlUDe$+g5-w~!yK~!J^s89h4%*S)ZG1fX3Yt159gNlOpE?n#L+oo2 zjoezO-*digrMBugzHMGn(DQ$=30}VW6Zmrc%EF#_*i9Q+e>Cu45064mR|XUd)H-!D-%IC( z7>o8pF_!0KdqbRCUqd^>{lt~F?LBU6qV`RIOTHVAPMrY!C*a*TfIqPmUjRL`-?2@S zqw&yj0{X6fzqN8dc1||wHFRJW&pNpFGS^!OzvB17=)VKtz=oQ4)7~Rd7q`xKmhF^eLxnt5hyTJV=e2X49VKF#OWM zVC!Qq{gh`W8ku}Fr_DCjAH`>?zcK;5i2rVvY)`XNZhMDlulX*^;9L6CLf&M{WEUk@ zE?vKW>8G1T2g&^tk_X1tek=aTU6GV@(ooNMGqKsCv3{R01R6r~Lj3Wu`09GUlfNg9 z5?z$5$$RwI4qg)ArJ27wp;tKD_9eQIsiV+X*WzGg<;j;hOSjCn5wm@(9-}tM%37{< zzEEv5d(Yf)J>;d;%V(h#u!vSayB1n;e=b^?@dBsdki0z!OgqRmcV+w)Xm!bGw(cAS?4h`@Lo^$;`(NOgMQ~K4t`j#J(z4$ronUj69(4Lc7oO7sotNWijKWhY!-u)K^j8lGxVoGI?g+R_h9jwJu8a8QLw@svbK|G1t?K!G zmb0J9i2=+LY|K;s3);KzDAw}(Vd1{q`A3~m6nJo(P( zYP;@kh`#h&4f~vANqGgME)-opPRqe5}=dxuzn&Z!1`^f-&O>pq_pTIQ2 zPrWGiJZqNn+M6+;KjwUH zf2v91;-_nn{yw6=X7xv3`1fYtqqolyX??!Wo>av%6zB2w*+PD~+o#}hc|}*Z|3>0! z-u;uJ&qvh3-X1hD)6`=1&)!LDTbTW{HNZ5Uy(%~GY{@d}DR}miE;9Q`gP++?Y9VjT z1I?b2gWNA6o}}1__E|1uZz*w!bM7a#>6{TCf-ES9GTX?9U9Tb2d6vPoiL-5utZ43x zjZ&;Hdi4{#A2t*R6Pk`wfHOqQ-*@?XNYw;^1O6d%;{@k&SlwV>vd)t4{*NQYxr78NM&8 zQ9guxC1iGro_Aku&mF>3tkg(kH!$0Icee3=``^70*>&H&5u3rg;TdV~M2OG1??li~ z)RfGtpGa=46|at{j@7W(^*U?XtB269qIr=c4~nt4JnxQYxV;p;O zkh}0$D>WLJq!$x~R;ph6W$Tr1CB3xb`AbGIFF`NX@m?!`FCJ&Z?)vD@0yp+7wU}CL z^3kiQ-K4!eE$CEJlZ=?qIOJ+3{LpY;aO=AJteENsH=LRoZQ$NdoND!%nU7bEZ0Mc` zZ|r0(!8gfDE#$m}yx)HNxHU17nk^Xs2JOL-pUHkI#U_~#<%6SpokU__P`3tY>$ zNuI5}wjamxJJvdIuBAQs9rY6hCp8CYQ?{}W8GR6aybM26dv-)qyN~hUA^?tcz9&GVHAF2Q$O(3+~bT;W)JZu*;uY@EPXJ&3++ec9UARwMk|{;IuZ8+jgo?41VW z?oMba|7wRLXYM@o_*J%??WQd=4@J%r_}Hcv-^hodeLtok|M0VTp zVY5bLMh_Dk&R9NSZ|`ucqz(QY4~*-EQ0ty{ck#|1et&&wjm9cj?#YIZ2YP$8VZ{72$K{ zGj_%4-eRnIj9tD@DdTNy%C}Jx#cMdzxNn-ESTA)V_1Y z{8dZyVervL8;4Fq0XA)b9j|fJ0!K=HMkKZe}XO6H+ zW(AFH1OCYK4+JCo7BCiF-^Yj8uYBk0tjZr|4~_mX_oC>}z}4HATgN`UJaW8uDC@q& z3&q>+yXDtfm2uwPm75a<$0fo=H9RQZK~7@3E*rYF7#;E~I;857ncd5&7h60rlKT3} zJNNEFhb+l^ZOhxf;3m#$`|@kPV0`AvtM>MyKibeAnpdda%R2PLV7SHZSGH4&U;*~FiWl=t_=$iYXcOBtap=~oz|9ZA z&3@4_Mb1wW;4aCpM>y_&sd0ssDK0Nq=hX=2M2Y11PZ#g`8 z7#`dYjxg@2b?}>upO>NYviGv432(*7ZWXl}8(HU<{N%GHC0#ai<&vKLoLAP2oE0Ny z-tkuXhMM{aihDH&+_C1c{%P!mk+qhQgUi~ibBwiZ8Zp7F6ZoA*$FC?hYrFd0i}8Gd8!6;z9ZV?k|mfv%jqiftfY)&)_9I5&!57>gCXGgsbo&7%!yVlh~w5=z#~3 z)w!4FZk>eu-}LFse2{bjq2phM7Zcho<%8tmiDNIQAcpY2Cq!U(W#v z&GRBD>;v=7z%N>n>2cHP7yUB+D`u|psGA>hDfue0VdJOpTNbf*=GPOp`(pK`?$YJ4 zKZ1Wv8^l4!&FKGjiEIh}u-3Xlw4r;&Nhd^Xe|da_d&}>%b6Knfo!VLePS6R>g}^ck zIYn08`HSjVBx}%r)VOZ$jg(k{;Zs}Cn-h3vH+Y$V?ry6>M_G=~!F@ArXkPLv@^9w8 z=#;nk-ck;lbnYj#Wq8y+XU2<9{oca&+CQ&vI|_K8HZ>=^lrf#{JH2c49N%efYIG^r z8<5`wZ6;|`XQj0AUK?$ibybaxXC26a&QnppnxEw21GRG2!$f5Db>bfF*Kk%3b5Q9} z%~R^p)6JPZFLhi4Y?c*uVHOOgJ=$08fo0%F@%c<2_$A&@$g@^((ncG04B1}4d_cJQ zIBTlx(N6r~_FCrER%Ub$wjpsB_L4P;8u+PsDYYKJfoh3#$nNv^I8(D=d@S!`=Dvc5 zZ#%Hr?)iPqyl3TRMi((R(7v>tk~#WWCmQgcaHTUk(`j)3IcW5M&heXa!!)C-*QBvZnaYR3C4_C$Ew|X55Rc zo@t`HeE&RZ=)7VLpUShTc|G?(ra7qlTLIti%(SMoSfQy#m-B7)fN#Iex1*=>jBoGz z%(v8WnNrcfxqgeSySwA;Ko;sY)BSgFLl#J8|T4d_7aW7>ZvaJTeQc&xu9cq6<5P8Ej}{%rc+)T92g zX2ZY8TPJf`#R)XeRSYDHZzLazfxy!vweFZ)h5leprM}eXIOA*BG5UJR33D&ycFCr$ zrVT&sD%P($T&ndTnN6+|zx$%i_d0TmuGHFP^MZQ5Wsb_ct(AA4w5%72wH6oMU;TU+ z&$MotM1K)$*k{_3FGU-QlSb4x@3?IwsNa}`@6}EhGF8kTuPc4N*eQG%!J_N?^wnB{ zzk4h5tp$>I$>&g$XO|#XTHkdquu>;)wNf{Yz^;d^7jJ|YpX6-JtMbZA?&PfM)?2L9 zRX%+FqDbm$Vt3EpUB9o&8fw;~HP=zxE;%1uVOv<+-bW6{z83J1hh7luV}aY8pS2Wt z=L63zHoPZ+sT-ICXPbPtn)-e2{3)2h+6`^gpri8WTkS}1SJT#kAw4f8Xrl~0)q?GF zaqi+;v=NQm=hBm2T4>DPz7>lVPFs0TG!lK1v|-w&9pT}o0D7Le$yGN+Qp7o4xM~ix zLkG52Sg9+6tSb%WtPGxw=l2PJB0OvVyZm|WkscE7NcyO2%GnjF^Hc>7&{slzLK`nU z>*1LW+6>(ndHhtsU%X*8=M_V9doDi2X)lX88t28(ruspTenmT6t|LoHdut7a{p@vTHR_2CB?T6wn6%IM=)coMq+ztUa{-;+x`%H|c@4(oT( zPV(c?AFy41M-4gv-sr%-x^U%v zmD~*CU;5UqKlH!UWLK_g4tbBNKa{m%Vvp8(>qg+0KyR<^u;Wiz@&AwhG(OiJNdHIJ zckAl^V}12#?D%HmmC|4R=g%nTp#@n>Xnc&bbt&*3xAwK)rL}`ds%T+(Nmre(qys*Y z94Nk6jQsa9r_8y8IVAElcSKfnHvBdbJgQz#Cw#gw%bJ=4t>@~RjXfo2F9#l|@^Nl_ zp+ybdNNNcBuhz%+{HyW}>3$!28a5)Ehls`0@x17AzT;U<5Z{>J_5M)) zzKFKA`7Y{y^OXm;)G=ST;L$aw6_3@EJZFawWeJOJx@W`I$v&g z;8J2I(DZ8jqo4V*nriv?GjQ-h;G(9Vah~LxBXT!ALWm zSKGnmPV|>};A}V)znHuY#<;Ab_l3duR&h1Sf?F3Xop&D|J~;3GmUZs`7rgu5w42Vm zx$rLYNY>!Yp0nrjm`BdYgoluK&861C?+;?bMQ_pLPWbsD@Q3bw;UZ*p7_#~yvN{|& zJK-DFO|DO6H@?EDc~|=Kp>7x$zQnZ{uz%_hFB2ZXJbgos4b`%a&Xbe~F@ zimQCt-R0<d(=CiJG3OU$@zD+RCpM(x5y8pi88<#KNOU=Ss#pR38 z33ov2QuIU(x$cv|k95Y4FY+7jPZ}{KDqVReIDSaF5Q3T$`OB}`D_8`_ zn$b2KSFSAHo6R%%EYkOjM)4edIyXBfS~N5#+Jy`iqg$(po0IEUSw#(c;)$2y9#(fFp<*+|Jt|^ZcmkJ4n-~#aUJhi;?7tOK@WX* zl-Nio`g=Tap8)#34j&Jnu+oCBwn5Ko@V1||<(<#1-J-vn!IyZ*p0j7f{~37|ebk>9 zzDMatdUG}H}QHgb8ao7%xw^W3gGF(iIv-+gv`qPdqn z&yA1gURT+u>&$c84+?wUzl{4nJu`;duNV6p+r|VMI|?$)+V{V*&(!FdF%f<%=eG)e zBOc)BnZEcQGCm+51-w=$ALWeI=;9szpAz8tEO1f)PQu`PB>bUud-E>&00Z6~9Z7w~ zeV06eZ1Ea;W57G;j>+yj@WvDP*otv#4pbdcY;SmMGv^W8vh3u=K?mv3bsAe%B$b|z za{7?%@1i>x(#S7QT6F&-R;oK{ zztgeGe!uM=c=Im%d$rR-doGMW0LC4_UJmSTY|_Y0{m|G?9p++;!V7(80K2uQND?bJIQkwc@2`CHRF&+a6&B9%*(ZBMmAh? zZrhf7V}SE|4g{*+E8rVD2V-oH^2h|U;tvIU8+LLqHYxs46G_!OvDC2+ecfEq1iDQz z&?CU1`+Q*0{$9yrKJ+eawn|FJa=+bDER-CYF|qN234Q3sXBB&6oz2Xd^U)C+pPS?P zV`qH%@SMgs7Jjqa5r6n}hL_P@iB8&AwgmQhfFZ@OSdB3td_23w3uD zpu^C?TC?i_M;)U>(Z%R+&DVy4n-)_KGZ+sLOZ-f`=w!9oiGDVFBj{r(KJpRhzZ5vt z#&ER(j3#aZjAI$!`LrY7L*Ey&u4=}Jez3=wX<|l}p3`?ZeI*$qXBOIV69=azesB7M zF0+W6xVWA*5ZA<>_O%GV`rXAN?-)AJe#PI*#s~1}?elD1*qdiLu@!LKu^xOPUnd`7 zE)r$#1MHHSYT`X@Jl_e7?L2?Q3PkrHGYQ`7ixu;&i51TR|F-}ue~)7yYeW6`*!PAa zyTGDvJFxA|{GQ;wmWqL~;?dIO^z#GlG2*=zY*2#lY<*|Ts($~Teg`f)SI%7@v48Kx zM!X4M$ma?(FFeQ|{0Du^8QZ>f{GsJ{PTz28D7v3Kjb-d%S6sUlIK-396|Dcro&!@Z zzQieb>0#DBezuyufh+Gcc^mR^W!o;|%tYqL#p0QCoGGSm;D&FTD;(Tj!x_u?CL#Qr ztBP#6-uSq;@~ZK~sa8(x(Vi5a@wqr(_hYkeF?F!EPr;vOYhN{OT==GT+H=qC_=tkR zg-7vv#nH~zwsd50sPashZ!*8p+|XS^tw$eD=6A)pw__j5GOZVa#KAi8r5_?L7F->S zS2;0PH)mS*unphqef(CJZ=axUw9C_NrDumlvC%&L0WQU_t<3A?MTA>)spcj(JKucWx!&kpIoHj!HQTw~;9P~rY;>i*&63`A_G7c= z_^9TEKlST3ttk$T_fn(WzR!-I;a>LYW9J&;+pu@u<6WcpQLyn8{3hG|y)m@m?7bbZ zPVu9eyhE(R)3PV4Bq&aH+H3%)k$o(VocAHC;9B^>tZ*JcxcNj=(2~ctmv@W z#I1;(JV>1gy|Zb=wk@^%y9l|tI@fx!6MvvfcAj=R?tymr0?8U=WhwHp03BOP`_Q!; zy1qXTKZSN5q}_|qZ6}xq-66ia6@Mt#`py`9RmBfhJ9g8z%-A)v*9`sqW%;+Vn<;8w z>wa8-{S|lEYmgatU~lRJCE>qkT_F-so$8PB9J|?qEoQI53&OABSQ&o-e6?Kvs=s~W z_fwLzn@5`&m9(A3Ue`+QxBH#8JnNeG@a^LMZw+jB*|5DU*tj>aRXea%(~jlBbXiPr z1eq^nGI!{}25W!jWsJ-AUv$Q{*8DPw4QHW)L)5>_CiiP7``3oVZ|`x>+Op#_rP;`K->f-_OdnQsww9RpflXft)_IVo^^va;zHUMXa4n2M#}TlV1FqW%BLN zaalJ)SK{AE{DDm==5NyR=-Zt)B9q9TSr3Fy63A$uj-Th~cxc}-3)$jtSI1wTi;kB( z^KAz@2A!HRv3~f&yB@era&@b%BmV;aP#cQrtDga5b>5k;cRr)*?Qv^;@fss*s(+as zpTzz7$lJfL=kQ$eHUT|g@t->%Xf)@IjOp{8efS&!WJUS8%h@BVH7}hfqjOYtEAKUz z{ol;7_TY15v7Wz+oD=2EoM2sb2Qk9Kq0Hz5tdYuIo;b(ZFh|ULSh8u?4T=tg99!1*+aA%>YJpY42_rHqYO zL$p0N6xDjP&Z^=3p}2vGK11a3Y46$L0>`tzD|E(;qc!N)dmo6j7# zAw)e0V1AV|FB-txHt@SpV@8+D7ZWeFj)70j+E0da&P|3n=f<`-L2SHxmh{Y?1A(gd zZorQ`ee%p^tD8Ege76RDu$q5*ug!KNQRp`24q?Vr4*tr}t7BJG@3rNl+`!brJIn=2u(@}Orenc@_jirc z`}d!D;3AKIpJ3}@ubyCQxwwAczr*upeRr*0?;(4p>h%w-_s|=)QoXE!^rG8)>xMM; zBHO*a*^T3&f%dxHR%T6`$-m~!$o^LJ%k{o}$x`3GHgrWhI;R(V(L0TEE3ru(_1960 zRP9{UsN9%d&dun6MjfksC0Q1~ANZq~=9>H+>!{;e6Nq)JO*xvQArs&xaeW#1E8C~DUX&-Jys!?|X}fCb ziSapWG@GH{R`tjE-r3f%^(!atT~7{v7v}^DA03>-qudR}-8z>tKH`>L*iPkkF9II# zp6n#}x8cZ-H42Ve;D9!R!O^$(+UdV@DK>@Ja{kIfbM~Q2%P%l*7EQc(F9h%EKbzd$ zLI>|1E3{`RyW4-27w=gE@UHJfuQjaQDQ=s?SSGG~YOmJdHWq{1LGa#798`D*zBIh+ z432)h-}yOs?>qL1pZzzI3!R_E`JUYgV&fg~@HX-<@^ud%cW_oyua(SOcuXg?3biBT( z@tjMqfY%Qkyq+8cuU`kRvt7LYVS2^j_3=LtujzWFV^vwn`ggLXz0BU->SQFP>%!mI z*G0dxuQeU^b#b?SUGhnUGbFIf3z#cZVWY5L%DdFwY}vJsqr^)B5%%8_@47XTO5&R; z-l+MDawZk0G5f05W|%rZCjSCms(7FBVKo0}ySKlOHcua|e`p{0JX-HxqFgrTev!G? zxuBtVmFM|i_1$O3|I*xRj+!0+^S<}9;{&49sTjmA;wZcEWLnufBjT^oliC=k@dy+(;38S7Zcmd zAimo=iYxJr3_DJ2_ClIDYX#5IIeEaY{H1Zg-#mu*fO#?FsG-j!eJXaQcu)Jb+2lmd zX*~I(xs9h@n%B7dGOO{aalXd*B!A;GQv!_*#H;PSl7VjgoXT~hBaQ3FmNm9*WnE=w zM&ml*X}bpb=?yi?7wqlL;olH`XKiKY46Y%r*^O=2@E)=kluaVP&yOAq0?)3|KCPA7 zbzi9QpgpF_+fv-9A1}c;XEMc_H-56Yc?`U9vBrSBF+RnRwB{jPYE1I+ZFq^{Qzt86 z`U`aDGw;$oZG7vuj;~v`Wcma2yC2{ADdtbFF_+MK7&UY%*NzTG7mtiIE@h0%CzdzX zOsXKa)E-0E(>aaF9}Q_t7L=E~I-K*O$6B#Kz?xD=I|s0B>gzCm_zBudkb|N1p_UL1(lpAEd?nt$RJoj0>)I%i1S?Yd7!M&zX1eTzzHKK0#fh8tz%6*|!Bc z*>Yp&ecHKOf%w(R(^C7id!^@oqPe%vI392Al^+?1=l9)nrYQGeVlQqz!9(oh)SM>I zozGO*^O+*n=F{ufnc8*WrE2@I)5X}=MA$Mpa<*>DH}d$CEZV!0^%iH}v(5(8ddsss z@2wl!sQF88-LS^qwN^^|lat7G5?_RU1K0!ZnZqRO;N^R{K0+=c{i&b6_K^84XX)cR z>cb;bz4+Ls4{S*iSu%ZKzmiq_tDz6KRfw1-q{IMBJy=gCqd}i`5i+`cU z0BWkH{@R#g;mMkeSaPzie$I8^8t}!GYbjYvB9lqxamsORtq8Y1XvfM=Y~Wf=zj$9NH)03(eg%KQ}OeGcQaJpwHbk$YNr5iUIhs zU&8G=zRwI?W*4t}<&uM?hl23c5JVB1l zgaG^Gct`s?+K~_K$C*!!{jc*+FyCmdnen;K-?N?T4O}&TSH@1V&rdQokv5E>bN*-S z-61|MnRo1fHq1Y|*F0`jzP)nk-gmBz?CbRnuRVy)dIvkieRug|+{1&zzIE5$ zur;i<8vfI_iZLq&Wa}UM{7gO9xbmHL{f`c8x7^e2!G{O7D}Ar;Wp6&y&O_uf!l!Qo z-`mS4?R^v2e#ZTWHsh;Hdb2D^~=cH};>9v%g+W*yk_$HqAOi%+6|be6|Ad^TTh z4@UNhAHl^m@txw1t@x{3z^nLHxi;Iefh}v%P3z&&XoR!hOW$kdIXq`%u!ZN?gzk;& ztjhJgt9WD=?@w5M+w@W^)V;BRed5cPO@DxW6rHB7aX9{;)REl4{d!^%>p~XogyOf6 zCnR4^K0xCd*_X)k6PAyk-acPE5qVzop{(T-rVFonZ;k1n{Qga0R0hGsVPO25<*&tPvrYbdM9H#DP3Hf0{`y(Z__e9v6=+xGX$L2gHmUcrYt==f0X zIrLgvP=3aLl2f6af>*#(9x_uf1bp+G_K@cfL$~5njbm+p{AK(`KESKU_(E)5DeGVh z;XC1aD>!H-hyMFq)9K;j*T!wIJ9{Pg0H?3vpJ<)5l(~1&3gW%X3a5(>!hv*9`Mvfy zcZGsYTFZMaH`w$PFz!TN4q4WVJ2;oOocNmZ|GW67y5HIZ&{<3VFTYv*X0Jo}aV-qbn|zZLwp(e4r($B!C)q&;KwBbi*K+V9+t2hX=Lh6&(#33y$5rtJ74^m_u{ zx6GbD+aA9i1Wx)C?o@AMEx5;jH0|!fhg#~;Y>j9{TlLUIbg1Ec#wzeRr&jxEa^qD_ z+&v%MN-uorJm^!j{Im3#v+T3$^tCwqC^V_0v~16FFZKS#xQ8Be;_fm#9Eyh{pu+ znb~VY|H1e_T*Vpi73gPTmdcA<3cixaWq*6xD;Vx;tHfzbYjCDLWN`=b+k&j=yqMljgaSE6s=3AXl0LKM3tKA9ndSA33cyYvaRXUu__- z3z^JEPl4Zk31munPwclh^6BO=)j9MxIu!X-9#u2%{)zIdxX$M)KG^t`em*E3a&A8G z6Td$f9~>;XFh0m9u755*@Dt-d8y`UbeRBiW)HwX0we>k)rd9|%p*#xWLFc6Zn;Fmr zzg}ynZB+y5|38zUf2l+NTO+Bvz?0j@QR3l!eJnk9A3vdwHsX9PZ>e_YNC)>HAs51Z zW1fxsUC7`PY?|oO#c#VDS-j22V$hLA=3tV=Fnh{bqmsN-5l>!(EZ$*cQL>89Lk#(L za55KJR6Pb;7K4r~20gN9x94ZyM;sqY?Y+6I*vR5-Mix2KQvDH6QG17ZXC`%Zmhjsy z?8Opfkyzk!HJmjnTj~d2Jexzp=7i>n36(dx8i)pXM%4pL4Le3?z zSnpkfJawZVy*AK4iaJ`@K*b8na(zt+|^_%zo!bWQhZ6u*;fqHoRBJGarh@)>8Y7`xY=PXuP6RS$k@k`J0F>B=F+r{v{a&(xdqzcaB|IPm#Ty;2YrQ-^HH}ZUNqB*&8 z`V-_iDDJvwS)TC^36_j%Fyo}g9ytK3&Apx1{%wsJy>0Rb(cMpktYg(H^7bwa`J!X7 z-w#~r=d1OL$(_}258>{)9?=P6&qoVQEcisfCA&7Lk` zQSPzy(|2e`KC|Z^_AAZeDt>n9ThVtPibuFV7p-Yf$@9>fc~UBH z7Fz$DJiBwr#U1~TXni&K$Ts7&X`QKY;+M7IOSM#dmcAE>zVglRspOk!jdD=>E;u`V z-_lsU^j+YgFa0K$oD|~anv=NN|)M@Kqoxe?f6?`T>eTiom()Qc59p{@yr>#Banib#W{Jqm$6)Vn) zZ+CwCf%EL2oOiaEYbgGY&hzh?>oC5_jX!Sg1zT?X(Z1)y;}3bBZ}L1}?|I(Pcb^$w z&3%~pL-P=8U&|0`D_#tZSO*HQ54U*;bM+x*Jd32R|D-=gX5wQ3J3hA2tOt$k%PCVW zOyW?c{%Kw#TWbcQc{y!p zA5@+4X3*y|SRc9JvNG01vdmh>4BBYLU&`S9wmPk)WON^7&sZjH5Tl3^pNMwQJ~6$L zUfSOP9Q&DnTg;Ez*4poDE&NH|_2zArQ9oI+i(ch?G459M;5NQnO+HvF-_7_WJWV)o zpTEM|iN^U5SH*AAaq+o-BV!kn3;ApQjU}(RIV`y&hQT;Q>l((F9D@!3$MV~3du!vp z!NxPXJkU*EasN8mm<;w91?_dR$>@+kS;_v<*4^a8Rqh8DbHH_wc4Y$=v1a71iS6f{ zi><^0RKxE9=&~DoK8buS#c-+lmeN||escRF;7EIYLtNc<4nm`Iwev+!JMKDwyUx%y zW(M%igg3HSw+l74O@^k>)@^$YG;`Z_;XBMe(w0%cgr1TOWZjM!lh!j?x1$ciuc;H< z97Z3F68_*7bd&Pf+qcbP9c(u1U~^b|oco!zM^n$y*gI;~DOb(8-{HC6X725p#Q)^E z*ZBjARc+>KvLNfG?V_A$HluJw8B7fkn={{p^Q&244Q@C%v0r@$(IM@*y)V z@R!b0QT}T_@?QWfiGaVj6g<$r-V1-7S^Z^i?+^uiAGf@-P^ z)B|q)E>{oOf6p@SXGLd829c)_GHB}N>&o9xaGix-Ic#OcxEZekt?#Ut(+5XP@!tVwKT zh&Jk}fv;!s2V^@HuhVY@L%!r9``^hr{S3IC$=-_gz~4UqTJCFoH7l1O^NS`%QU`%^A>T`v-Td7%c0bqh z+Y}pK*QXxu^tEV`jo0c)_z!%ykT!Na!+aC?B7Y~?{RZ|1`d6EpOPKk>NbB@Lc%tnq z);qhbiLu?Bfp#0QfD`EUA?Ua2uYh0tNp~J*>q48?p0MiQ-ADfqUaWjsyEe~@{O#U9 z&wW_7Q#l-yt(Ew$C6dib_Q;w%j>!?8MM^G1MlH{OUin-;TI);w^64kFkKdj4J2 z;^w{p`j12QM7sj`b1`dadf%;~_B!t=-(ozt)Eb*~z&K=J+|A4*BFd%ncjur#EoXnQ z{G&0J-S%RBZ-c*|ywOTMdGkP8K2N*iy(eeeeQ$?npJcB=D>Ap6aqH|j(GEEnSFV#A~OdWM;M{BJLGlkPHlJj8SM zOkPZF0G=n2y(BtMe?vPn_mDp8Mdu|dY`BHL05&1Xcos%DuTJeSFG;XoeAntvHtXBu zM9>XMyD{4=nPtlE9Lb?Qvj9dSFQ+zXleI znmis@ZUmN==*-6C<2L}ytj1>PmrzDyV0f@GI6AXYJRzHt&f9+YcRO_Lz@9a04H`Wn z+O?yn!mN?Kp_nrEP|AAnKpA5~>MNfvHCwfl2B>5f3dhM58nnN$~o9w;p zslETnAD4_Z^Nx`tewX|-ut!$5ZK@+Tl9%3^YrD0V%e$Z1%jbu{jbcuh(w6vJHc0WO z()+ANwOvqY?`JN(Z;0_nMBBCchUX#Yo6<^q?{mR2=iO!fd|=+agEemQGSq(Q9roVm z(#2`-RC8{I`%X1H>G1IybP(~E=nQ1J0a>ok4Ma6RE<={{kmXyZ4%`ndyliptf_@QR zz9zh|ze?x3d(=ta{*H~<*5427-`{~>fIjla>!xs4FSM+~KE8rp_7OYrY~Hc=5y)4ln}N?q{!QI%>cucNA2KOFzz^ON1@N!a z_FwaT-Hp71FX!i-D*W0x$gBL7vC!)nH4|E{LAUES)*8T3rD&)9sd@P7EdgIs9`*k7 ztot9+o?Pi7@_vpf_e8K?qxLx`rwka}_BW!N-1hg;z87}E{eYFmuwB(43CAaU+8wQX+D#&h5oEE!X&3yM^KL6FE1IB9<5$wI`cb>GYu>hvJ><7H zpxX@271_o9m2H-N<`?s#SPy))m>Pc>_(WsC>$Wq#yYGNsTA@D-$KPS+@4Dx;w>@ZW zeRrJ|Y}(KBWxS{Fv`4wkY1e$46?tAhlAZ^8q`;(hvNtrdv=JAJ3a>*i_H zv+q{=K>HfS_pUy$&)XPSzqEOlW&GMTU#x#u{P9)xo~_1a*m$sOk81z7Voj=NrkZhM zRoe`G+afyvFP6i5@&_a24kS4DEQwzuUuwx{>(O0ja$@a_N&Bm{cR;jXJ(}}2W@pFd zK6Ykv8~ba-3$~uTEVksgc0QHYYdt7oUU1-C0g|UZMk*{j( zBK+gFH#4LDUHRk6mAhmZ`n&*H(q|)l=G`A8-JqBN&RHdE z1ST%9c-g|~rpBV>zxyCOqc`coF8oEn>9scj;Lo3ke9@L<_jUs> zYnAY~g)WocxO?&^4h;sL_w=^YXO{;*U1{wr?ofqa`V=}zbJGUP-^Bi|rW?*YuxY!M z87-jieEN`ov-Re*e!z*IkN1o}t)Yuw!9y`zK`-Z^T>URpd~h` zso>1oP4&pVX!%?tetaS8inOJ6g0xe{o^0x}7f0${zhxjkS2<(y@>`?fxBl_Cu*i=o zhu>Uy>|8vT-!$iT?VH_45L>hZIEgnmjX$#vID=7aPg4$X+Wcj|H<0HX_>8Os;!l%X z=H@FcD!-!4Bd2M6Bs#kJS1 z3O`RVoGfS+V%#C0zv(c1*A)mg%{jAiQ!_Qq3+QVcaHyuTeRgLk{`BeJJGgMp3pf0i z8DHlax66OSJ^c4y9{%ex`mUc|*vVs(0r~W@%c9rXWi`{`$GdAi{3kmvJ@EtRAs$S6 zcrXC(sEtzO$j`VWN4w#{6SVO(Z6x8r0N;E0a65ds8u~u=6!sfFYQlo zhyOmr)+J32uD$l@Gx{$QDOp4Anjia8ij1`YpZsh2ABy1^y958o?&K4$UKum9{C!LI(3o$=~JwdMUg{dfI60=dOD zeU!I7H!9Hcm(4g{&OhbGCB`1yV&QL`uzaV>uids~hvkp2 zSynJTJl#6oO=6D&qwgZ)<;qw)WO?pnfq*JemEbS`Z((!TgfAX7rNNP zevS0h{ks$7n7oA#TS{Gy2f236qmBUngKFyzg}0s^K|9c~1DWV#kBR+#n0;Ph z>QQRHm}>Wg<3HsLfz{N}EXD85X5DxweDFc3Y7Y$UuEw@}0ItS?*Om&#gJ08JMH~q} zNiJZVz&-;WQEs;Cmn81u3e1Unc#pj17GV7sVsi4m5{xTR!|#kUvDD~q^ZOV0UHx5c zT@+Qk`9sG2)@zS!`2c_CqXmLvc=rdY`&b2TD)<(EX&5jPC!6}=t>kjvVsZyxAXc8* zO&m})k2y0Y^+oy=Pq^b$?4cPy>q%t9=21Iu^<(f`5r77gUvOow32N>hj8`KA+P73q zoA2L34ZvIN^}RxLLW@L89k!ktCRrs~}#B-A1jP=InRIjD@4E`9faaIMe z+HHIU3`SoVJ+0ipK>WXXCY>xF=c}BD;p%N~J#P7K|IMC06W8I)DYw4GU-GT=t6QIH z4s`U^rxMLtgnRJr#;C<3jqr&4Q+r)A1OLtCmyGyHV$I#?w^M;{o4J5soD8o-h?%M0 zR5N%_LX#SBzKF3W;5~dg_SsOE9z5>-gn2nM)Sj=l8phAqJC;I2`V&9&)5f)}cFh^# zQMQ|XbCo*7@FDOyk+b=V(9=bnmGj23)Piobjk0BeX-9UQ~s3V?aDWA1Fqe&WyIaLhqXRlYW(+d zU;VpZ5xwcZj#$nN{$5NRM!0rks6YQBJMOlG?-kEOx0v~jYE@`&P8BiwozQqUe5v}$ zE%3n}-bFtlXWVVy%O7?v0)PA<-`V%nG38$M)t@!bY+JL#+}p9EZQS?i zE$58Ga+iGB> zM^gIcw>;m>gM?t=K^7^up3a;`|eVVVJ=h_*^0piE*cSi1= zz8=!~TezpPgT7nnb0;)YKWblX7UrG( z`$dWaqUTz`w_weJ7V6_E%W4W!yWmdxH#JCTr(@i}@hf&AdMX!b1vWu-=v%R)9oN(M zG}bS2H*|NrgfC5dnk#i8iyclo8N6q5n9_LTHnoF1dcPN3&9tqt-64KN-k*W4Uj3t3 z6#m$NXWri%l-mQGYq%Dz*t552#kjo_S5WUn@Isef=Fab9TNH~@9B3l-GL%oPJ;w6o z_b^}X04L3&A-w$$z~Sa#-D%oZ49_3GoqO?_>jRmyZIE~QWJ5f1Wqj!{^!CV?{Ilik zt@g$bThK{@sR-F9Ffe7Bx>bdo4PD3>3OEaTEn|?q6wlRgUxz)ogE3U$*EX_uIuqRd z3VkwR#ZJCa9gxi0HyM}S*LVAQ?`h~<#dorw{qOJUd;cxo*L#XdsNEv?wUGAo&2rW& zC#)#o-SKH8=;)$LpKDRjo|g>O0U^pE50VBVE_tLdR&@)7vV`y_CZFVu9;uVe9!BFiTwEcfg+jeg1aPZ&P4)La5Z0t>( zQGd4YpW%DeS#@-|Z*?`==Hhh6wX#8u2W z#Io7S^=yMyn$zhU`0hL6am^PJ_}Yt@i&kB0*E!TY*XBn%FG=&nfq%O^<>bsc{;TY* zXmo8Fjs6iDxw5CLtM85us_(D|M&Ip1Pi2@K4B=dOvw2fBp8~bd;x8n5E?G%3=hvE; zYK3ZTuT?Py=58C2)dFm_X$M(7pLT>3)i~7mN3qNH_v|s|-o(u5a{+oW51U$Q*>JS* zOtGvM;L!IvH&C|6E7PtmDl$I<#dt0Yn?|DAm^Zd)^-qwxRai1;|I=fFeKdya? zUfaT%eY+jJyZDyg@UAavKC~0Ls2dBYJm(P7;?#!Gw z=J>KVousc)A3hCqEJpt3_<~JU!~weadn|t!QP-{t-+Ur^Qo7+c%0)5rUw`}~?$hxg z96XkW1L+(uzY%0({FnCGSH)43f35wXig&jl$GgCX>hkOX?vBamjWe^NjOkeD)3V29 zW93K7-?i&thT<%ge8>3Ayf0t71zA?yTCrpk4@C~;o6r7z?>{Stp#xo1%-D+Y#fre0 z+Oligif_a5@oEowL_Z(vq&@BL_qHh^)AKN8=+gLC49+e4t0mXmt?`F&#WHB ztz5kAFgUdNZQEIJSeAyvA3OTh=LrgQGzAos3bng0lK(X4~l^d^ydLH2!ViZxga4U*ITX2ETp2hU6t3 z2a|ZW6L{S5jx*!3?P}g2h1Lr3ijK%kTq5;`Yv%>epd{MT=gd9^h<{cha14D&8PO- zc7%N4f?u53d>#Lcp36chPtz;j;|d zUqbHDF51|AfU}3ti3j1SU9{I=1)~kbuC{^eP0%cX4o{-nrRO6B1g z9Eg`GZ^_I{Gvi<5Uil3-a&_s94Jr|>Pq1%Mv_|)rJm~N(tZV*r>6;B)UVR)MOTN{8 z!NxFo?cwddlJEikf!i>-(VFvri}=m}edzGjt4?3(`5TS+Pu})5A5|V?K}OF@8T@Mt z^t|M=Y#kOAk}y=m}^2mZzb^I-AqqiOm_ z@pHi}-WJ^1;8}6?*TJ7^cZ+|O<7Hy&@UM7XcC{OOA)S1P@u;>v`Mv{kZS4H^Kzs+Z z9e`uTB{-*e;8cA@%|jhl9l)<$hM)TJ8{8f#*$|Qzq;kg;KWv`*!|GaXQ zpG|uoo7B%&)OQ=vK#ihx@Lwi5)D7^U=y}4r@7L|*NvN)N5pB&t zmupXhbo+kVkZtvki=={+s0)Pdlb$aikEN9B24b8Uml3Z85BMET6Ug;j4gQn^@ilNE zot(jUnvXt#4pq*M!TVYV@9Q1BM`xt2bKciF^qaibZ+~rlhwfE;H>Uj8Uf|N%Egk3& z?SE>MZ_T*mPd75IE_hdQgol7Vhq2D}Pn>G{-RbnZ$LaU!8L5$;eiO*B@wMS^&3oI~ zv($m_&1M{BVF*)kcHq*e)-iW^UUD(5M;@9SVlLwH72Nwr|x5&Vo-MtfiQHt*!;;Puj z+2H+G2VNHrFOECl6UE1b>kSUgWy5xoGvkl}Sbsi+a-n@7gj-`8|FH>>F|JSd&B4H=QIE&VC|Ai2l|&#kl2P52P)#9oxQ+=0Hg zb4Mc2qrXP3!7tqakId%Wdeslpo|-i`gCk(koOmOAr+H2}xYwGG@+;gL@=0tn`&~X# z4$-B*!H=S^9rj+PK0m6CTn_nB-n~r8X^a#26btIBD-T`74;!%gnao?qubj|h=LcYG z(Cy6U%vc25I$$F|;+XvC0(A3gzDY7J?W@yyi>iNMY!vYP2wDplN8t@yr`q`@iof*Y zBMl{vxB=YOF~$TkyaC)Ur+!6fXm(R}5FP}U6Y$;}|4W?E7fBrrW%m8HiM*m>UruyC zvNZF@>$a3q|K|>DLZ~cK68bc2s@9d;>ECa7Me~?Y{6poo82B>dr?^k&n@4>8_Wtkr z))*q^f3AMkI*KzTmaWn6t@S?iwo|1NNFom>eC@lwYAYpB=+0$zxna@6@TgEJEl zBXU|>O0^|pTY?}Uv8|Aq>BVWu$(0Z<#M+iqvH5?#Yww-xM4)#5^ZcJ5&lC1K`?A)% z-u2$r`@U-}^uDp#1H--e5`yqn2%f*+3Ls09I%|-nZOGDD{1(j0v+!ME`-XhjWU9kA z2YU>=%*LTDCqPCC4l}Ykz?mHE1ww9i2|4LcB9~|IyBj+~ zy7}q*S$76a8GA*v$KQEQ+l7{|MQ!_O+fUnmX!M15rd*TFUK_gqD!9=YO79m~{^4gN zXMmv<7?cAn-ExX|w07;zqiV;de>ZkN!ZXP);AxSJ)7~1I$F;g+k_=73$MZBE$xzR+bgLF7@=L2z!LdU!^%?54uxp7PG-uFSJ?B-1Nl< zzjn0k&TqZdYK7|8&tq}XxJC@$^=xC5RU}%Ty>;B5GPjAA0 zex(SzRpZ)?4A8n9$r{Pn9>#FXMa)O~W@-J-t{&!h+RsFOl}%s!N`9w5CP6RU(=!_{_I)>Z<0-)kO`O$U zWbC~DO3kl#k}p-5#<=iZ5o=S58nUC?Si4q_T(jFKHf;+RcT)fB(3_bn_mV@%Tsa%r z?#`9p;F&~%OOJ}BPSz)~q4r>xoCm*$kPpkCfs@jY(2Zn)=2^Z7d2Yzj0xL8;gpX_{ z`DU7jYVMhY96TsH0NrsMn(?6v4%IF@m4aMIrEbYyd;&`c(HCyKVc=Ty1GFH&opeJ9 zdz!Cfj{3+5x&gVY9JqT~^XTkxaFy2NNH58U^jqTG(i85zgSFMCW<#ePwYjIHyG>lc z=nM1#eTYN9xxl*)c-H}MYF+NBApX~fxZg@1c;;#5lk_n_AKiShET{i}$N9xJ&Yj#t z>VF9S5MJCdzRVfp?GCP8e2?r49Vzf<4Da|n`%CXjKJu@BDA>{Jx1!3M8T}1?A4LwN z`4Tu+LjdUALQE0m2dmOS-1bJ<<&;@De2j$E8kt+lH z7~^kiT$Ph8`$yxO97FfNBKdPM<2$eq+u~kq*KaF+W1rQ*S%IZJFZihdKYie*+QHA*o~jOhW_j?# z-t+dJs^F(a_@PbVrw06#kKxA}?){i@9xbE)lg9Kv``Ce>nZghDL-uQdLB`NOo$;dBpYjX``ECfhIRV{7d|~K@y+!y|=f#L_-Va{7iK)AE^B%m==9%!4 z!+ADm!%Hjtcnm#V(aU}>@Z~XZgdfGwP>mJ7TCo+)k?j~uDAs7FLy)@L9c0OY+l-jzm-O@4^Rs zD|+e&A6uYR(NcqhgFawwc5u+_@R*B(ZU+Z9DL;d+n95jK0IBi5D(+Q=S)NBfI#I!Z%mt zl>UaYYiF;3r@VG{PnB$J>&!Oxn?7S>|FjufrW3q1W2?d2vavq|jV?ovF9g5Ku%(wl zcgwJ``x&EtV6Qqu&_{cU^DGp4|PD`RTg;h`}*JcJ#-$Fai~x_0>2 zt;EAW6)f=X*J(%N(#(1gbW?v7wgGmycv`WJ9|_k6FTvRJ=j#8abM*f%yZ>KT{}0mt z#_|5=(tqTR(VI2k;d=C44S1*l4>jOn06eJQnb-sPM3*j0ZaTV*{-4ix(i`IW{;7?- zbWhI|SC+ig{Sdfv?@NE{`Frt}6ia=-cx$l|e7KZd{LVbRaij7_G`o~QLJXB`B6*S?s&_@w5I?bcQEk!Au3lKuzVQbE-t38!Y-RHqyvNbE20}R&t@=tVeK0Unr zI=p;*J3Kusd5{KOkK3ESdjq}F^*}ZqXw&v8FWa_G&bxN=g z))(fVfCgXtHo3#!AU9t=Z}6*r7lz22@xXsa-JDakz)!AzBDrwf_FP0?BzKOnUspM_ zaqJ3g4s2#~1urG+JJv&6XFR#{ouT(L zzk1l7Urime^~dK|AENH}GryW)VUzNw_;n#|Dz=)#UnBm8y^N8cIBzz8_$m%F_dcq* zqjKkUwfN`4n}V+M&Ib=nvmVntNUMtI+=q~_bV>k501nA zO2sWDMkfOIE9JKYoq6*(&VRug!?W$jqJ4_J_=)Ghhne;nsneT^qE*+h=IB`KQQ1>1 z%u~(1V#Xm78L84aKC_Wed}HnxkZ;8|<<~|M_^qOiS&@;xz{*`~JDNAIMi%yF67S-9 zHFd7biX5@a;7s=xdG0N8?%lw=i8{W)c#pk*1HM}D)^~4Ym-&w0=6WT*N!nC-*_G(c ztSv|PSV_Zo5GT>x&b_}3nWlW5fzhuY#qZX+47*`HayU7?>1eWV%UjElr=p{|v@xB! zs+>LvzE)`QL1c*TFQbpT)_@DiaAL>Co?nMOFJGJJwrFSb<^u30`O*W;$d~#F=?Czj zeVO!q1@+3`W|#HOq;(#ABVF)w<=AkI=kV?9Na7dpxdokMf}bVu$8q)p*BPwO&{o(N z>10JRy`dtT3Tbaj@Kekl&bM!9v zSAg?8U$}D^yFKmQmJJVD;m#$#v=-5W)~IRi^a=iy2dVd~p$!XJZLeQVB?e)i1-^cY z_1N*tnLB+L`)~+aeSMOzi8{vf7aji--%6H#S-i|u^MLwExahmkGJeZLdKzgh9Y0xNlV3-lpd_n)L6cuzjW>rV!v2iU*p z`tIvn4p1Hdf9gLzlLF5%8Fkq#&v<8DC zVcLl!Ulgb8g>Ej!_P;(5fk*7MG#Z1MSxrYfu;f-klfK1#_-ZaECEq!oU(i+I8#>Bxq3;Ck6gZ@Dc0t6in5YIi?a zciHY&si&5<=3sx6*3EPF{w6j~UwUXqbTR|@exV#32j&6F_S(dh*sHpqsrbIhN!Xd) zVMW*Wyw^Wl=b@)NC-K(~Lr>%R|E=NR$PJ8f3GKT4wT$t(9~zN7+=k3fPG>wa@HwGR zQ^{+t=UFSXG1#$gb0d6Ly7j@$jlQJe9&&+%qdLZKZe9I?0=_?v{rC;bXVzj~divoF zm-@g-R^-SV;QLMfGzVAiV-I@}WD=86KHsOXYcu^x!%yHV{WNEoUs3nP1^DsmWlxDd zP##dJE7OX27j|~3>oqO09K)HZB-x>X$JqI`+o}DrwzcwQY`RNZ1ALhL)SA227 zW5CtwpIRRwjv^U(lJRQFjQnIEu59_GD|17|@}WaMRFAg`x0)1N$ig8Qj{UrQM{ zlka72-HHWoqI0C*-#})n+zURmhW{kLd)fCucyh~nN6%m4x$Un>ij^2Tk(^13eR`~% z5&NWQ1|1<^-e*iX!;}}8vSO%7v71a;{=%f#Ur@IFe#{Xmdu6vPua2;`(krj(fWwtn zlk@Rp8}!1DuHKh?RqmtgndI}W$E4%SxfeXnx$xwq`Cbz*1}Cgtbaj~4gOfY>$-WHaD`Vy-?o$-*_y6)o`B5taS2t88i9DZCHv<$*v6FcXL-Q`IX8~7=><)-c8qj1YxrV zEnHi>z@N=tPOC%InLPX;Hk)*lw+`3O@Urq>&?$$_)m~qFkn)7_adPA9JpI-hAaDMW z&Vcr=$JbhXm+xg;46>)0JGJR?6?Gdp?E_LjhN+ZjC z!vXS8^nNwGZ^yJnTfvy}!TQiokMpd&9@`6>R6fxcv88*7yUWHk&+f8gT`E`H#e5z- zs6My;Ue>s49;JIVv>~5^&gWqdE!xq#;H21KhV__Y7f&Gyt>GUdGqS9WN8`}+wN0v5 z`$)NCZ|?}{<2ZnMXu7Q7q+FOA?ua~nNVeVSvdT>csRS_6F?%DBH%^!sCY!_;BMA0Iw^)D7Ns zPqwbc#Q2X5uZLnyjE!>JP2GHxiR=p?>xVqwh;C$i4MOMR`2;DkzeG-G?>g=AID>J` zW?ZKWerH_WbAfMm=*c`|T;=;zS@fi<#?*^bTh4`JA^1q;09QYM-gkWGTP-t!P3;=j zD#pIZ3UuZ$Zd_Yr6PH=m%Azf;n`dyAL}o^0qy-+dRM*m<#3 z=2og_1#m9GZc3?}Yjkw219zQcI~`~Jq4c3!kG@$}hL1zCjPmj-WDxrqs$5lO-)pS0 zuNBlaCo6I!<rLq><8VPT0#UAw!zl?0tx{NUE!H1YP%0`tP+?sBUq(k@m zy%oK^hq;>kblPJ~bE)$vKZXCNhddqS*E~=A<+QJB1?|g5(0wDjfX~P(2Nznu;cYLA zb_D+l`c-y;HBwLeYF9Rbu0HIzJ?N-;fUnd z9CT=-7F@~xO8#83nNy1IQ>~|u4q{VljMoYmWmdZzC$7#Whse4p8YXAa?V}?{b}CkU z06B68c^Hx-&(l^7^(h9ZvgC-yWkSEpBJINQarx{V8kc;Ui~lis)!f}5qrZZ=7Wj2l ze-z(-S~{0^25$}jS#bYy=ECjGZ?}?L z`stJTmfx})hMni``}ViQ+8sI&O=-N>xb;>Q<+r6;SG#R&r@lg-Y5kAxRTL=KfU`Q$ zr}}+H`hbu1Otdh_d6%MtC!mA=YpulKU5YK(IRh>&tUm`%PY|0wCoRyH%g2Lv73QCd z77V=5K^by(jbndQ0Hf|p4?YRL-r!78mo6Q9V~j2j0*ma8=Xt*d{ovB2^h0;L^_=cE zLVGTMx^M|5wV^R+b9iVOvL?jX?qUA3)!^58r^=t-rq~(w;5M%Mu7)u=5VFTa_@|B| z%Hw!x+Qc!ap>KPg`eozj+vW66XO3zNdXO*u2LJZnFE0LPo&*0I&Vm1a@NM=m^6=d( zVC!ek0kz#oUvyQzIdb4xmA&}ZZ~5k;=fL?256&wco&3D&0q0HDIL^oIUR!^8b(y!U zu}OhXUAWFh3odLc&H-Do2e!%dfc!lxJ!ouM|8$`jF7fIf>}Jhh)pqT2#X`g8yb%K{ zdNc0mO?#7f53k=l9U^o9*-=uOv$kd6=G^#%A9>Q?Lyr*wS&(CYAX z%_~;mQ`mq{L3OXVl2|hGdc~?ayESLjz2#o|Bp*`u-0_r7+{t^g6Mq5@UgS@B?d1A` zQ{Jg7>uwxh!^UjO7hO(nSf+e=jtz73?`<3A2tI#z{`Zi{zaL+ZWqiGk-_GkBm48^d z!h-A1@r`!FYnoH`OckF7V{a+vFB8AiRGVKkhf$1M^;Td5oy|YGk^Y&!c;Xn-)gLF0 zq1c?|`RDfUrAhsB?K8DoAior2+Gpla!C~mz^sgHGteUk4g2}bvi|M=ip?j0(OqUS{ z(H!@2p1Cs2oiqI(_NPDFoJsGG+qkwr|C4S%oiXeAojqn}?Vp(skFTHb_D`@bbM(j8 zoW3oiZws+^&_5H_qv&0?uf6K)Y`d=-Yxw{s&fP+>hVz)a%|7egZKpeT`z-xG+uZH> z3H`U%xt%|$|4%smXT8rE$9J)b|4j5hyEv$>N5{}l`HOqtFU>vE9Q`cWCLf~aytZ7h z{lLm+);-OEiHknlE83lICBzpOlSfh*uyY%`FJ~^0W&iG(4t^MalmD87p96nooO_v) z8Q@aSsw~cOCJr1VS9d1m0p8nh1zS{)a9;te*{4UJHRIsG=7meVW6zu2b@w*Bhr#b; zzW5vA%;AggIe2sVA%*i+z4H|}KVD~a#&3ZCj10++?#*dBsyW^D|sy$nG=8Q@^EVC5abs`qk{3wn2E8u4}BpEU&=k8frpU-jLb3yItFEoUyV z4;yPJh2tMk?f`$gvF8=bJjCypv4{4v29t3M{0F~(PMqQuV(kCTJgSd%V%@yED3Z7r zT_AYfeH<%+NA_VgGC{DEvlg%WGvwlATF+*Nt?yWz^^{4ReGUGod$k7cXNwXA{2rQN z|E}&@lvsaJ@s03lo^}qTtf%wPJ2K4 zhiF$cA0fxqYRZcKf^~Vlz%d=aPZfS2UFBaAY${jMj&LM8Yli3*pXdX3k~JUI`0-og zr+2FZHhfupqj%Qh7n%utrf%M8BFCegIh)0tO@7xJU{Tw8_i_4Ab&KtPtGaEx&3(+_ zP7`<6xagfq#zVdomCHY4C3XP^WAdE}>EdM0!sK_5T>sWftdR%t5xV0fd9{{#2G2Tm zZy96tWJa)a{Y4SRtB~<3nlN5Q?ihI?yeWR@i+w`yQXYesF*9%%PlWds54=^3S&j=Y zW2Af&^9^G#j*HCL4C}$8GgQCmlD^6I&*h%V)eb&H6Z^r(Zl``dbMc|F@X^DZ*4!@x zCe>5rUj6yzH@d=4u2Cia4UMb(RetEKlK9k9)H?$_S3+OHaV7MHk2}!=UFm$JO7hlB zIcrL^5*jn*?(?IS(AcNI`{-!WjxVrgVlQiZvRT^`r>$=KY<(D7TW~RW{K~GLZOZXy z{SR@2uYB3v}%BrXz+32 z2Uvi4V-Y;E)`ov$Q4?k0&jS9*?f>X~w*R;PU)nFX+qd<#tN(0#yhXj;;OZpT$$Icn z&b@H;Uk1RIp#m3`n@#*wG6M2YygI_fM8{KuxIkcg%uLvUJHTFR((YV^T zqw!g;$H(7|Lw^4NFtwE~^4Z&*uYem5>>t`-SI?`?+u8m%458Octu_u(#d z<{7dL^{wbUL;mAG!?&HLTn_oalgEAAg^b@x#`~<_8i{z!2`-Y^`$l)_2Cor z5l75mu4DVeea0uQXNAr)KfXghW$4`To7?Zi86SfK_}TOanxCHKAM3lh=geKz*SO;g ztbjkKTjMwi0F!wayx6vp9k==4@cEiJ6g(hbZ^)&aDUrmNC)rOAne}1g^CfiFSUjex zKBD#B8>?Tnb4k$eWYEw77s3DD!FpHIK5*$;1stl+@yBm; z{qgnWc}P#0XTU5Op>ow-(2TK_UEZ_jDuvLvJr4G7AHOHZ0`Ie?TkYU755a` zG;yzet!gYv)ct;Rll+EVW9uv(+-NTB(Cn>lU!d6|^hNf*yB~ZW{(kR%@bM~a560A< zCq<&miIdDBE_R5x7(S5tH2hYwN0x){<@i+wiG?NO>pY3i*XI3X!~3%pH_?08MB+`Y zJ%5Gu4XV5UZY!~;#2Q&)cpwt3p$)|<2dpIK`uaU?M~SvJ^IbRmt~C%35W{^MKl{_f z98&H}o*ui7{i5J`$#?zb-S^jB_BjTbG39Z7^4(8${q(-`rt8@&)2!$a&-;k0_uoxU z$UW?x>eO`tyJ46({A>7828q$XY2xyVZ7cRUCH8vY-d)N;SbksR(PQ|An&(+dAEI1< zKlspGauk+Z?Zcca{<5XLs?$3!Wv}W%%G!_hA^bi=H2K8cTAbzfKBRgp(dUSLHtz8ZP-g4#QGr=+K}s1JL{N$Ab9mi~tC z8d(oDhjNC0YUi`o)Xuj9(>m||<6|2#{hZe@2U|Ody|!m_O=Yj`SzOPzM)J5`5o&4| zp1%a%zvP?N@@VbdyOV1_wtIfa#(N$2>sT|C=gTmAd7XdX)aeD;k(D>H9)Hxjpx(#z zN6^s&z#u=_9gJfRc^s_kFnMswOTz!%sWocaS4DdPD?e)%eYeNAshzmjgt4xHCY7Ud z$f2)g@Pghg$0lyItl@#`&?jL~@6=O);^ZJ!5NwDe68!t}`!viPj?}-UKkQx)-dkX9^I9X=Xdj&MAagv-Ega?xS z$(=p;M$K8R^i^x+`d9-xP+@HzTFP!%!t^(yXkTT}aN{N6R>Y9#rrmV>~eF=34B zpTQsW(xvPn2Hp0-|H?CLW-fY|JdOzWGWmT79iHd2Iyc~lkqPC5_g3%`&JyIR@fVK7S1+;7k9|a=!_du& zx)rBxe7Cgg8D!8i$g^+Vmpc6?$c8ED=ePWX9Q}>Rt&Fe!byr4uhT;8GcytIJO-Zu( z_fG2ji7%<;Cyaq)!xVp{J>74$pPyv&@z`_r&(}N0;NgdV1`qqv>zhuFE^Wu|6>MWP z4E;#{Y5kxr_eAgESQ7Nnoo4R|{}JV*F;9$x>x(IGK{u#;q3)aWK&;p_^N#)g1?Il} z{`sbC-%m4TJNG3_S^V`E&Q)v6=b7i)TP-$ar>?Uu|#o^%1h56dAN-!DThxW7=!P(?eI6av??(Feg!Fc8ozn+WkWY5DE z#UY@*m3hRnQyBMACnv$HTNS%-?H2F;)wi)W)U{vSy@UPEJjeC-EjDKrjn8j-u{~z3 zjy!tsQ~;ZWbt~}SAY&n)z;UA&wdY_s_Jse`H4)w$1z-D^BRv7Hu;!~XnRSWEp;jzi zxrVLaFTj=kVmhDXPx}$<$Hz&|bPG91!(no4q~EcLrj6Mf$;eR4-|}?e)HSMSAN;0z za=1Q09YeJ966^ND@az5ju7jT>zYoEG;;|Lz3tj8cC&-u1dY^KJ(>lZC440CtIZ9p1 z4IH39$AICG_!AhCBxAVRI>Vkjm^^9TPeu34t}9=V%3m3BL^*=q_YN}m=>{LtQ{}vi z9HP$_eU@K0X#Bd>^m&iv8{P+fjklL{hW32JbE#u3^tX(^J(l0JXQ3~>ZFbP6_qS;J zdT3pG@kVn^iWM2!SMG9hY>_G3YeGt#_w-FZWxJm~)6YuU?BlP@3cjbGqG4~Fb3Jul z?x{1^l#^q6F1aK-J)Y*z>wVO>qyIwmH}k0PvUXB7dpxa(`-R+(Bt8$HXddp;r@bHD zxc^>0}A3CjuMnc%kq7j|lu6)KCi+!Jhr@p82*JNL`ipG%V%H6Mmev+}tORa!e zdtJ-g>xWcE2kl{u1IY84{9Xc0N$wv&?#HP^G2th6KD1eRQkHCx9L;yau|<+Ymt%M2 z2p(i8@Gk=uCd5h#dZ8_6!OQ zmqJT1V7h@n(Q)VLmJJm=D(aq40PIK zX~zoeTguo;k<08eh$yrbH4C zJLBs5SlscggJ-?-C*_S+Th?>IXIXm%KV|}_4Wm7$(VBei`8AV0zgoKkBR$|S)MQ1I zG~Ttj3qoAmPM3CtSc{|g_GF=t;q|sG>xlZSv$a&G_Hn=;olqUM;5S6OZOnr;_wrGn z=t_C{2dG21kUr|2dfzUSYtC4&!Jl&*Ykl1F$e%oA&*7S0v-2L;z9t$8#xn8!c;Oe$ zdZ=fq!67jl%8Omx!6V{}*I7#|zBtqVpt3n8<7+>@fgt^!iTp_d-nOg*n~zz(kv7h) zV;{p9{#t_ztvMAA_HvM;W`JGIzEE4*#Tgfsr?3hQ6Dtnh1pv^ z7Y)0#>(c&ua#2OMRoEr@(pk{#e)2hVuM}M-T3(2s$Ia31o%cVZ={Jd=%P+B>zUWLY zm!{*$yPny|ym{EvjEOtuLE&i9c%N;Yf9}-nuiL74fPbb;1d)zM$&YxOb~9gIb>rF9m8;bQjWE8zMx=bg8qlllW8 zv%i~i#6-)IZ4Xj!IEA&i)cdrvuchWZs`o+aRU4{zEAJkou7|MI6nE(19LioRk}rQ? zAAj0AO#Yv2WMeIRkYv6aSlJE#7NFDCl2%p*l=1cGSvEO=Y zXD%?`-eis7>tC8$9*mZ*dU|*LCFH8mU#)LZt`y%M`CHmj?xg%c=6ht)9Ac`+sRKhX zs(tQKkn1hv^bOa}U6A^>YucAkN9y1D+S}OM+1+17->5$Ml6!$qebM?H)rmd1w1@g^ zny_s#TPI;JK|42HW*vFG_Rt1K2cD7y$Y|Q0b=lDlTFK9Ztfptl$ zb1i!I2HG5^-ftQ>(}xeCH+7D98vR|dDz^J}`YTvh)Lp8xW{#-Ox$lgA-}F;<9pA11 z$7SG2{HOIAiZzR#UHJaflv9nIJL}hjfc4kWKA1TYO+{V?&VTWJc^(I$o4XwkovY9>zoUZ&G znsmNKHT@86Yya|A@?&48&-kccS%&<%pYhSSyo7yc?je`j8ftd$xzQSV0b5V=iDbsL zpMCd3e;l13>VwDmEkC4qTlH#wHOl+)L%zg*;17+C&X2)+qtI{>G#rCRm!u@OEP+So zWT!sDz-}4XeIXn6CzvZ0A)_j>M(;h=J$Hp=(Hp7(Vvr3I+ znsUg{%_XL6+k+pb>>X#>Fh6*Ge2l^A*!Vtl2K%MYwO_7_bh-A+BFbm7VI)8QZ?|Ex z$*+;#^y)S@Kj%-`Fu&$o*)T72H8_!))Tkm{nhndfh zpSLGlpdVX6>w9_hy(SP1X*&*mJ#49>UA!49^pzu$%*rw{*4MaH_#ih);Y zFPUp-3qAh?&&u52@Sc9(=KhvFz<0Os+xTDg4RosYAvSH(#VZz&xo4KV7hooCs)K{9Z%52O27NYmStI9KVJ3)NYV# zuYmzyAak&Q(d&WXwa{JwyRtRjdxt~ptU*t&F0TupOzHMHD&37Kuq-P&J900c0U-KPT9-%ecSAPj(_;tIPdpu zi{yWf_FVbCgFboZF9GPY8GF`-$BA1B_t0b^dRBC}7uXg5y4$%L`xCwQy!r$U^agTb z703&dH_H9zxIdvj>7Obaw*TY8Ru#$DK9=73=t#h4_Lob^7L3!z_w=qOUUe^dPrzE6 z1TQToKDI58KhS93A8fV~rO-kn_S#zTZ~OVQ=4s4FxKXi!Z~yA_ChfTzcm0CP;gj1e z&g_9ViH`({2lNLTcFp9R5A74x$XS6s_mZp4IXmWzXyRkici+S|dWtyK0ruaMA6)Ac zwYQM`4x)X%>;B$GF6SWaJ{_qpnk8i*yKS@bz`3XAfn;$>l=q%M) z$KDjFFOuu@5_`E&m(~eOcRfRWdwgl8zMEG)vm4ue{uLqS>#W;-0UO{7ANg&m=e1-c zMOM9$pJD3jE`}}&hyju((A&s)ledt|Ml7gr5%YQE+iTb28~Rlw5sXV7*?5+pM*PxG zzqY{VThafO*c4`M8vOS}h_z@u^ZhsbwRD5GMfgYH$$nxeIx9&!eSmw?Jb+uv_iEZmRz+L3qt z(*rx)^8y-&C(6D4>BT&Q9`LzK{*5^2jpZ@Fnta|^f0Zp?j-Q8oK_2w=`tt{tBVV{b zSpWXy%clRHBwrY(cro$?p6P`*Cd-%Q#N=cb#Nnqt-fe~l*24qiydz(KC}#54dENy7 zteM0=M}ySCvoZdGzt4kzda4|`1>FstLvBfCNmd!TrFWg*0eB}lqY3|K@^Fl?R2wFK z+gp^+xpkY&Z`$bLo8J0JVk5L})1q^}81MJMdxp;WCJrxuAGq{gh<-}1OXs&if2Y&k9Lw`f?!q{_o z;qpn(g?Gu~DtNLC|HA9ccV;$J?M`C7AbO_061*qThW3D!T_it&<}(MGC$(V5I}*|i6Fdgz~f{~&ese~~@Zz|l}3WZ+qg-IolW z1dsY>!!vXDZu;i~4z2MLkzKzdcT4%* zTcFvkwDSb*ZF@PA(6cvr){VSTZkJ7qu3z7dgMM2%K)|ish;N#*9b4UD%JTO&?0U*` zf7{sm4ZBQ!6@5{^|CRe*IjDI_#6#Dg24Akd_mVlwew==dtU(?P-hi$?&Y2SA4=JW2 z8_1>WKA!g@xAcx^^gQtB?au&x+z0K6#zpUa@M=FiE4m*^{YJdg1n*?RJ1;=v z?)|5LVIRChU*^WabpbKY9q4cN>1mfQx{onfAGRjUnFQx3GFrO30(~r7UHyLndq(%F z0-BfE{r8WIs{cDHF0|iS%b1yWrt*&ZKQDrh-R}Fp)A!YkmzO?%`_}1AZ}ayX{zTIw zTu<`%CV#e#!rp?WEFWFNt|3zvFK|u;WiM@;csl1L9^W3^Av(XGwV7k`o4q&&I5YA1 z_L3bXz~Ig$T>0(JCx*bCcRukp`-*$de*XmgWcIMe*6W4;(ber+W{~gg*>~>tU(zmf zg>J1mV13LFJ~wf_39+bGluuaANB!7n8oK~}QF&&7-0WoAj;y)4Brz)u`9kjUJJ^-w zHwP1O;1S(PhWvW$oNn6&Ri78r=d-OZd&v0L$N5`4ET6-RivPgZ@n+5`ho5DCm}khd znb5bZvo=V^KpXMq!hG?wbguY0gv^u7);=VXyRr*pzk0`cQEknFiDgSYF5?0U)ywo67!Cf8{X5@hI8$lbHaTMmkQOn9mWZ-O&` zT+YtY+M>v=L1@p&;=cFJD;{>ft8~8e@ttJZfj?jy(NAdlSu@9YzqS#0Ije0X{6V`t z;BXdl#^94SdibUnTrLJqFW&TA@EhBRZ`7YJ!kfA;JUkIbezN~o4*FSkQXeu@u|ds; zlv^eri*%df4WsbzP54y~;ZsTZ$XB}_s;z5yFdQ~9ja2GMBW9MD-MP8Z=NrlUc2Yr2$N8u|2s#KLmnB|ot|=~=`=Pzx{K^`5u{zUCaGK?lZSOgS;~5bz&oV{WGc)S+vo+R&VhysBfx~ zMclt2`p>t%vuQ8u$b951`xrN5(N1?wT@Nx_w*Ak^1yETygm(C|`9X5(di;U4*L}5X z9s6IdBPLx^JE!4A_Pvx{T8&?)wIR5Y@0PYk{43Wop4A=bsK9-@WEZre%gjA17@g^3 zZxY=rAG>#?pE2gn{`=-HXmz1#^9?P_lGz>C~Sl(2d9#O zVQ@G(9zW7N&Gr}Na2`SPhbEjy5P#J#zu?Lh+165gt#ZLK!Omb$^ z11;CDzVFdw;(D`dXEhA4H$e@1&lF=Twpsz_Jkwz8?R^t<#ui|c&Um{kG(nf#>D*T= z%}>8I-)X z+f!0oE}PcW-g=+4qjj%!s+#>f9wZ0tYbjPcc{%OJX*=b^$t@@FJIq}*dv|NlTDb;X z-p@1be^*WY@{Rh5g?9hUpXfj6PxS5cCwh+g6T(mL-M;n*!oil~w3SSZWijW#=3T&< z!5LONzSvkD-xm~GBTp~~nwyf+k_J4tpGWR0IBNBsXZDY~Go`8h80A82gvJZ3cF~Oc z-CE8q%j3Hg{$>G_a`^Ay{X5CE+e7~23BGxaK79SdDJ?^*cI>``zTL63w&KNz+JFip64(ezHR?T_7`Qh}IR${`et5`bR z=})|YJ$Pad{kMm$%zl`_p;5T`zE7w2+`gRmM zxQV{2pQ>X8{aWHXzeTvNM8>*x%>`$|r`A+7e>m7uZO#i0)^Ek0>Er$yjSoKWL2dLZlmEg$-kP*R#8M}+9 z+n#e;D{lhMKH5>+{nYmqub&(O$~H7^mGjp1b85Q&!A$^{%S1`=MC*Sb6oXFL=tIH)Z*aR`0sS zlr=|My=$o{+j*&NoxN0bN17)MPq4irSPk@niRHHwhWQ$^7OBVHO{i*9i znAg%EZq*v;0eA@t^9VxLPmdpnG-Mc;NR@9a4Fa$)xMY4jhmu#eDB%#{*-)0x*vx859C zCH?nd;MM#|^E~6f#kL{V8tX?7%cdGwWbeW5)vLFoXC5S$I*5K*!rqe>{gsVahCQ+Z zdt`~P#-w*rux#vGOc*jih24H>^+IQy+?tD4L?B_Wizl&ox%SYGO z!WtTErhxK#7*E|ddb#N@qH$*}e$~%d>q(jUVWK&MSlhI5T}&Kc{yW6ov%r5d_2hv6 zCTuI?^Tei;zpJmMsXZ`_+#&EIT_yWZ@z}}n@-lpKHzC_}es&&fN3~a0B{Jx6dblNx zob`jX^BNW-9~I|TygUUQB6Ei2e@S6H@{sk&qgC2VEz=h;`+H`RPk%cyLbl~R$q#7b z_-yFoM*IYyviqTRXiw0Om*~eV`r$9N*Pso|vl2uMF@GoTe+<98O+R+bG61~Q=%^y-XC`=&pXLE?PueK$BqY% z=VxiZPQmjK^d)#W&!QoKjUJ4?be$nXxq&u^xWGN2C8$huzy;cIuDGB{mI0=c2PLwZRU3=@- z9wR*_{x@ECq3Ztcoamm(btA`BcZvhQ=B8eGJyIMn_W$B+?Em7>1pB}H+sNqe($@Ep zU&xRg@|E3rQ2?2#xm=SuM?5#W>Xvslb$=UK^<8Aq_mT0Dtmyb!%79r*nVAtxyxr@c zVC#Pad}|IMeQxh@ZtuS=ovxUL^!n@I7ysv}`qtg^g-lNU9_;rQ@WJTY;|ZY8IAkep`4 zgt8ddW6Woi&&a%EWD8f(iMJi*eTmjwYvd+$Q*$n7#Aof>+(;eG)FFEYJRJ#Sh1+|J z(GT0uv4MMbtqnGJ_O!CEM5g>~*RkJ%KlZi%M6b3Y*BPVca1i|&)EsxC=D5+d~J1CDg3}1mRKh5^`MXYZ=)XO-pyH7bUFL_Z!!Bl%rJ0P2~O&)2$v-K zm}^UBrr=**7Y=qV1GXpAgPnqBE%WrX_B?%~^4Dj+Gdllm@*>oS_0XzvegA{;HM+{7 zNsWQ%%uip&Y)0VDg|^l~TLa8fj$(i*!KQyPWcO5KqQe)#4ekq*D z`s3)cUY+R6inQai|NA)Sflm)c*vmAKuX{6A7kBxytad-`kZX0skNub(F6@dYTk{K; z+sENK(Y<>HiEmwLqQ+^@_h6~nzrq(`er07vee1%B%m>4Xx%fe^0RDoowQ?^y@|VO+ z&jk0E@?#g! zwuKEF-pd#%55MVxsM;@d@aFE_F`qg`|ApX9`*1|SoA5r1__h~!vI{hR>ok7on@6B` zuRSxw{NL+8>t?>>=HVlcrkUI&>`u*>1FK6D0dgM$U*t+&(Z$uJYw|?{{+b zgE!kp8F1Ez*?zPj?@JF=cC6hjdDM-Z?vd{hy{;Vp--%D?t9*k#`iV_s_EA(j!KTv0 zOv~RH0-kkTgTUmTXWVQBT3niV5*fT7_;P?x?TID=z!xI6C>#b1Y~JlJ=Wx1sBzo+559U+FjD;RjYP>`F#XJdnGv zOR_z4l{NAjed=dkI)E=g`yCvoe-F)H(zTBGVUTy#&bkHGksvZ9mvzXBDQK?u5r;ps zIwV8M*Mvtcc+?An`1hi7JWnerP2B5i%1;N@hf^Yt+`ek=sZ`2K?kny}rR`MOUP69- zs^l5)iVl@iqFD5sw7=sz>y@*>TZ_zS8%Z^kE4y!0fTdclh=b$&8;b)p{-y{j2`U<$R?#KjbR%J(E2uUwkIHuxrrkW6$x&S7OuXdEYh2M)X^JHhU)V?o*7JU^ z_gnr6v}VgC_Y5YR2b3GN2Hcr31l)JX1V;8-3Py z>(O27`1Wz|Uc-5WgN)riOM7GlSYO)s=z(-=18bu>bBcLBauDCsrY+Eka_ovP;_NNH zU1yaTxp5PCF42B^;7+{b07h9mutIpAeOJn;RvNTsXB7a+^4;%c%=+aDZ6u|yk0}Sbc0lHH; z#Zyi)WqVCcKxObCf1hG(qWb~xs~DvAGE5P??7!ge8C@C%)-97@m0wwFb4JKb$s~^D z>h6Dx+j8>z&RTd^f4lNBuVkXUoGx8mTdF-epKWBWku{rfM%L-RY#@9Bk8QmWz9ZI* zUvsH^X@%(B;-#f#ZJcy;ULj+Lj&5S@mhM-8m%^ptL<#RPuNf{E4`H*24WfZL;mq>l%XyI<&;m={lDl zJv{#%;`+)Bh@dmG&><1*5A8d+5Z&2E66QX8?dyJtbDuo5=5-m|Z`>tcZstq`3OM;J3P}IKL#xy1Iff zEoavVy?XhdSYQJ{DTfsQWb}eW8uq$5Ceqg#*+{O9=`lxR-_FEYH z2ENfeq5Mns*jHfhy3d<X~Pcq-=!_HT3g5rU);kN$?{B{vzKNsKap}kaeY!ZBRjB+z`W!(#NO@^-y zAt$gGI<S@&b$0s8%5OyX8=Z;nB;U^XSo+cZ(nngC_H)KIgiPN?{W;7%0v}j=G(ej^ zaOekzvhkO^srUnSE_Dg7%wswO;9U5$=XOD(3mTCjufNPWWR47}qTibDHPYWh$QF0L zr}JybZ|Piidi{nZ#weM3WLsu)Uw$&l67PJk7n%?qR3S@z$P&LtmhkTUFEa+3=lL92 z;zJH(qZ2gW+bVkro!~_%gW6-hAFEG9%#(u6ThhlTMYJZ)Nay4^~X~df0 zk&EFGe{?4PxDmL{#UJ)O;}(Ud{Uxu&jqy#d`;{IvidN*}R(Dv3Wgws&?Bft8+bk zS_S?65O}(&Q}{c;)oZJ)IA+@_KX=aIwVZ$Md{cQGlh6Okq~1*0@ezwkx@axyXIYnu zUoQ#ydXRgi=!EbZUo_?2V}}l8MB3;2B8l(9qsk5LMbBz&oc1o6+Vt)we%64sam8<4zXo}*jq^WV zfOq!76J_vBF*zI^@Q-ZsefVhhAusl#hml+GPp~rt{~YAqL-0=({DVEysWrMjR zU99=ZtMJcq$6un~FTzLr9X@jRbld{$@<;54kK~Wo2mfeX^=%b4Lk>2BU=vI^v}O1m z87!CuZ!UHgfjxl?|8876VXzb%->n7~`*+1~; zUFbmem{;%KOm+gx8#XP%|~g*Y>qoJBu-w<$Khf$O;iEjCKRVap-5Y#HmN>ktbK(NAnT_x*@=bg@2z?xcZw~?k<<5hQ z!69G^``CjQdOU=_IwMb`%Y~ma@$_HjorR}q)9|$Lh<+3dlX?1=*S`S`u?%Jx^?vi;A*%l8BCpUcau_~uXX^6gH&@8xB?&&pl66281f0r;5?< z6MaAvW8q(L`o7QUySM)@v4(4Mewh5AIp9EcSqK<3FHn54mpWES|4%t{6q$H8C91(8PvvtHUfG&|Bh)0Z3zLQ`fP z6Edb3+-m+ZL~KuUyZ+g>-Q%Z?BF0mAk1(d$?~Xp(%lGn$HiOGv>Q#Q!6o)GOVrio2 zOLlz4TQ_?O%{_>3zAg9QX0@T5NVQYWn8-h2Sl=A1M38{R>V@E@mqXgr>93pgu-KW;-`VPobn_9d*}S=Z!`u0MV3 z&@61s?Tp7RLsz~S{Pe8!Bs%^T#n5}P9|EiuxP`vUW}1QA4N!;TdfKDSr+jf@)^=Z( zoRXgSAn*Sp`{DnV@!C(!V4XExdCtQX=y-eX@xj=mj8{2)*?o)r?bx?md&;1h+aihI zGG0&m^=Cat44vFVol)^6b4Eq<$M`2G>z!>$oPm-Goj9;=1J*oejJEN;^60ks;YUy1 zs<$l(e|u^q@piyBA!nlyd@E;T^ajhUtAJiPr^O=Q5gfvY0rJ3m7xA0tZkyTd+_bLWzaOE4WKsPGC!N@7D zqL($?D*^Yd!N|x7=A3T3TW+L1g{(%1emh$Abq^H!^ZuOPt5&9~;2zRbe>W6)fPYRkbJ}J=@ zN$)<=Ka7HfwzJo=+KFQvKPRG;y?1txySy0ui8A=GlljYK6Le09&*>4 zf2J{=L0^kH0@0$=#~yrvg&JA>w)o9_{gv#jjA+H!8Mt=dp4#{i+L)ZrH2tG%kB(Gb zJ6y$jDe-w8*XbWyd-Uz9Ydin-4+T3~{Z`b_)xL0|=ODKHo^YbSiCsc*bBAze~6-4=1Ka>{#^joXC#p$P2}#>*i7J zu*>yVMs~bY%oz{dU!G&_c&FJe*Uh86&o0+rY3eGks-1~I?ojT_^Y4{o97ro!b zzQafMymCqTt{Pi)88)kYxN1*lnO-#JqRvYCQbv8)vP=8;wwmwaKV?3{y=S2DtgOfp z&7-&BhwATA9KzSxmK7oAEM(@<1D(uGnM12xcxzfezssp#-_Ky5);)ZSy|mQC{_qj~ z7J1r3zXf9@^O0)sScWX$$~%qxmJNKbV*|@qUWv^=X8({^CK?^By0%4n;UB=W+Fi$0 zykYoCYoVO8)Ia3%mexU$hg~#@zjm0t8OGN$N=7Nhnnhl)OD|uz0eX>KLw{?YlMfwa zaY(e)bY1kbJkvbV4-L*>9wNI@_;z!}Haqyvfe)lRvLfMx`PTZY=pw$=yOLWK@UiL% zgTJWrjeh6AH~KDzd5QF6Rtf$_Xuk^D&tbluqj6;XW;(Q}GklXAT2vfFKG{{&XY}kD zXz{bqqS}%FOF3ks$7?;bc&&#Pul3Mk4s(aiugS8F(=Mz+hwAW zcWBS05y`}_YmF_o&wbE@u>#xdnAPF5erLiQ?MmUB zIhxi1T!78=b`?1m{tJe)$)|Wb$BItFuF^ie-u8x=PkZ&B+Pi|>sBUaZ*^Kf@&VWvW z*qbwnE3U6y)Wh?veYV_dY#`CwGU&pduLWY2l=ED;Jvut*(28`l@|g|% z;9c-XDmE@`l;4+YF!b1QvRp6Pd-nd zCghj@8RP#mw@u`i=-D#YUteL))xkW`f=3IHg;)LJjQRc>mGA${ zFJRw$^8K%O;c{qbhC@SrE{=12h6jD*aJe|{=b0<7>dpYq7lEf7c(fiI8E9+^)&y+K zW=(+B3bapxVdf+ld=3oCi8tfyz)<0V;kYx-l4bhm!jp25@H6f|jwqko3Pej5V`E@@ zV8auS&WQS$qjYN>cGLXmU+NjQG`1eUFOBBwH+Dw3Y!K^%(W~?u`y|WQn3>U!^PBI< z%l)9K2ODFjGdD6ehUQI*XUf+2GPyC*|Jm4tx-VN*&tz+KV>@Ur1{l?~U$Kj{^ z_GuhC&YOUzt-=%bma&1cDK!q$gd^@59ASeQ9P#X2R)PRvsH= z4#E8*_QhUHo3_lbb7s}=h-3}?J(t`y*?emEH>{V`xh!6O9AeJn^^cCiN26s{euTO; zmTF)7m=@7Cw6?K|{SSJ9MKH)e9LI)<^GzH(G|qPy({3byf7vSdFVUXj4CRbRH~5t; zr>l>5Hg?-ShfMLgzJ(vMEGu7a{{fzG>#+M}+pt&u`ke`TNT^RgqEE`pcJVhkAGr{? z*%P0+t`*I~uWR}P{3pBdQy4s%`N~drzH+AnS24KLyaT=h_h-g)7kO1KZvO>1j(Nt^ zh3EZ_XV&{1&+mEKZ2aFC&&WmpE8}^||6n}#{OY~qx#WL1o~zF?o_`>pK>YrG#}3)sC_?}(0P5`8~^E%j=$Ua7JH~aX6k_lv+)N#?`^MayuAwQQ_hR2 zqYNFRI=771DH<1#X0s+G`<1JrS*Kec^vWAo{w-nc(3t!KhW)^BCiyq=U#B;H)59wh zB!@8u4T?so8w%(syFmrcU^7rc1C%Y%378^ilu4&J-R z>$Lke?!(+heN?AG314v%zRZ_mNv*GAgqdl#*0Y`=o8b>+RGL2MH*eVKQ#izd7S zzj)vAH{|TDL=WRfGd7S}^X~X3yziNH4e06(%(;a`@yNTLD__o&S=S$K#DA&1OS%KO&iv4}GsgMNzoUPjz2Ed7 zSd%%bv zh!^y(;hjq&`Qn*bRz`HuMUngi4a;}4R^jk0e3Of&d4FFKU6f(}CSGuO7Jl30rnRhh z=1d%``DobZYe{KpKAOjMi4|$zV5J_u;l5kD4m8y7*8A6g;Zt2^zgLz2vb0P4%bNQi zi99lk@_z0YVdGY1Sw}P1yxtqKZ#RdGlqxohWxssKuIetO(Q`mEgT`GQS;+f1_l+*q;?|ApJ zTt<$ZVuYEcKH9-gumG6>eFq|``93E%AQyeG&YCqLH=qexQ#=&?@T|_17kqB~_9oy` z&Y15C=BmKuzrso^;(f^$BWwPQc(!Hw^P%W#CG>}RT$5QpKOXykKvzI!3Qp~;^ z+5Bh3vrQkc0q48zy7BBV_&ifQd$-e;iQO{?RP0`Or=KUYiB(U=P0|@~L;m1n22ONh z54y1z9ocs!xiQxekG$4O3>L6Hv)D=uEwU0Piv9U6uJ@v=Uqk;4LklO#1GyBO$7wsK zq|nZDF#A<#k1t>J=h!a8jQ?xU!EbqIa$Np?ryo5xprd$RNk8f%zQpY%zC^d#8$R99 z9d$~pt<;TIN?^5RVqDd!jtoKUgB85kuAjfH75xGp3KibWD zuWbC&VeE*^cYP~|@GqaZw=kbGUSo6K9lc8H42C$%0NuV(KB$g{h}m;ZYYwy@ML)dL z#@bQU*U@0LcXO^l6MEUE+idbd$n&AyzWrKL18-86P37(f@)7!AVx`Wrxt~)WzP;zV zt#3+CNtWoH@wtSXH)KDXt~JWVB?(=t7L_o5-t~v}JKI^4lEit-Mjw&4tXQ=6^r)$= zUZDJBKRQC|JqPXqcg$5J6V}0d>zcBoLk{d(hd6LgVSc~HkbVp^PLe@_OKqAxwLgD0 z_?6Fe&#vD=w~`BP9^mFK#yy$*AL?>trLIO^2JHP?@@^u&7nrnv%XTaAcJajhR{k1Z zlWZHk+m=O1jK7;xKL?o>UNdV#PJP6WJpc{#JT9M3Qmojld)1f*V%L4?-A!@U{=dFG zlK7UdDF4`f)=KDP>9<0ZFY~Y5OPu3dDU`FVmC)eQZR;cLPo7tl-#2RIS0N)q-T!uU z=3C!->qTG3Tgq2wY+hORc(KVb$7hu2t%Bc|le;vtD8IKkkl+0{vV`_~z!84wO}(^R zE_xb`ufY1Xr!`dzGRZ7$9?amYZ(-k*=}Ugx_v-p6;t(8FucK%Da>dIIO$d%QUH zSib+4w{wAys=D|6o=l!3ycC6sB?$onf@0McQ|!qk0Z~D*gk7P_)`(&ruKB z1cQP`D^oltJ#ubABI3jsMXa@+Tfiu4Q(M%ww)ONlkGvqDVzn|n%>Dk>-g~lVGQ-1q zFP~4Cnf+Mnzy9n0UjOx9Bd}5ZE#!1Jy$C+T%i!M|!8ha57%g9kxu4DbEaLoOWO!tD z-~I4_{Jyb&!5(`Fb7@)QgZbpHX@5k4FAyK-jQs}2GoO0jrOYVtcu(OZ|(@7a08 zE3rRZJf8@Sx@X>aX?2G7+OZe43tRHBrArQOCby{Amu+egRU_l_$@hvdHgg{C1;o9Y z;0NT^?~yrC#-X*cUs?tCUPjK?W#o(%a_0L0-+)GHBAYgp4~-4Y8fIS0M&1|QD&ILw zJMtGU&H*prW(l~-12+-cwB^kN!=GKw9v-b5>?(pEX{WCyRCdb_tMd2TfuSeuz6hV$ z7r~xFvoE4^Ej0ZAI3usS4p^cO0Lxl<*?~vvSWAG%KF=Jzh+K)WpV!t^{+8UEk2bBK zMxl{ww*{X6_16M#J?*Qytr(fy>l?IeA$E2QKE921M7N)ZEOF_-(&06fZ9W=$#W6d+C)un`=0guF01M`?Y~nE_CA|8tL67Eq8}Tr zxSrbyjlBYmy+ZwmZfI-*d^%x8x!G$X8q;2rYrs<~zN4!c1NM-vr{a6-nSFAwAHL5A z->W`9e2eE|Q;?&`N$FVe(;{%S5V_wESB=nZ;@QB~S>VdmiJjc*oC8j{-+2|E^WX>c z$<^%B>o@iW#*TfRZtOQ7YwVL5d&B29_I(-lTPz^nCf?S5iwJhVyT@Yn`OsUX!`F;g zF{xG1+|zt!+CLSbEqDJ^q(*INe{5dMKdG%6a0UM7^8Pin^NnM)BbXBH%;;;UR_)BN z+X+LDKRIf*^)#QZJvZ(R+n!s)wN!h~)Gtrj4=Mi24phb4Ep$J$uir6XkAe%?$f_xj zg@0q)wAqeNV(%+6IUew&_M^rapVm#sa6Xg#XAAwZa!7!EJDg(;ovgsOaeZpyQ5S3= zCqO>y-LyR}k9uqPJyF^ywz7K0lUExqHt|Yh^WP2c#=xEGhRWVIJWs!ysSz(4l->Wz z?Zn&Y-|XkQo!^WJn%li}nQE)%_w2+k*-2lzw#dl5yq32{!h7s9G`|T1G|_p^&BLC} zGx9;l@cbBXIEiPM{iS!C)~b^xe6W9B^aJCRU zCUD1d{q9dS{?G13mDA!r2Ts@K80*ZnA2`Z@GfO^hnl@_bqmp=;Y)9E>(o^a)Rc2nT zc_XG|;Iehp?j#*G(Ux5c9XQ-vUcUp!Vh3M~(!i142FULbZ@V^t$<>$sKM#H^xA83f zFP$yje+!@JpO(kzPc&fsY;tV1M(}sL9Y34=+L1ix@~G=?UrRsU^(CLfOQqN?CGe7s z_v(YjCk2;kbAsB;3&h_~)81<->-EajkS?m>Jk7dE)`1YX?(Ox5WSg&g@<`K)V%2~f z8;rnzVer3T1h)Q~(}N4CHyVv`rUd*a{qe}xKgCa$-C)PoZMf`pcj-{|p}rPqUq=0D z!EdpDG(3{)lI!R->6XEY!;B0-b6#4s+wjIF4*bT!+2BC@m(CcCe_CUoN2&bNPY0ix zh*3mtMtd_;NQ9rrh@88>}=%=OfE4?FGJxVzk* zQ{~7xwDOEQZfNBt&)kY;TzUzm#NWj;?VOpS{O}_DV#!f={YUqFulj;n?bdDC*fEOD ztc0$j*rzr4zLM3%;Xktc4_T}+pVncs!qo>vhjJgeYUT{9go)D?%F_1 zWj3@iTd^05v+k(5I|CfdG-sJyu>FYtyPWAa*B$TwX1ovgjdyqH_r_-!uN@m&Y{#p> zmEx?$EfqV6RnfNk^v0^leHo^C%V7>gzkTtjoR&v!JlfQM?(Fv4fpz_>z6y8#HZgxI zpabQCYu-2Xn?L)##}u<#I-awD^N8(nekAx(yy{&CU#q~EbdC09sjdm@jlgC5_ZJFh z;&aB)G1<0Rx`a3Mw&Ed*eJSo0WnBgv^7$^wjJ~+n?v>zTqJxLdtLnK=JCZlNtDIqt z>27eb5?t6mof`+^++f<-!I=J&=e%;{-Cy^~k(;>Z%3d!&z4k@PlH~gGxt9du7oTa{ zN3x4z@W1S&+#OXF)LEO-2_AEIaNcTd@Xb8mh7a>f@(vb%WZkAcJ*(iSyx&$;bXniu zn)h2@Mcx?iwL9!<*c*M%-EOY=M&R3?36BmldUuN(yAn-j6Wba?Z0jXrTmLZaXcKX? zuN0$aWM|#Q^BvZ~pKdz%4{zmu@W7kn{`&I|ALTjK!ji9i7wzWq{y5&>WnXW*YeC-6 zbkDqxzS++H)fMnV2Xf)AZeNArAL@E7G}pdwuH6-=s<>-kf10X>oQznPC5^(<)9KO_);l|3VZr`~yAYcaALPvQMi-oM*< ze+BQC3}SBJhb(X#Mb`UD0`Y^|d&K(-Qu@zw-oM6qeoCMW@KlTSP9L?6_R!=Pjpi{eFeM%E8~xfOw8G3*jr*4$r+WfIpkn zn0ue~{0sP;*AO?iRz{$k*pUt%wRdYX z^RNY2G>@&|Q8wL+;L&Rn_O}ge8}nqZ%kS{En?--NFOr*d-D_iYu;}0NhPPKTf7vfpRXmcX+M?)BaFqR$uOb`%9z(rR*FF*6{wlg;-s*X8gUPGU zZm}G^z4Aw5ZrlqvxN_~^tDSk92JB)H+UF0S zFyGP}*oMQBVYAQMt6y4vuxlQEUOn}+E6D|>hN5E1wm!^9-{-Z2Yb?_bc_;_=<9o%B z+3oY_mp<;Kj|OB-47;rX9IMV+8M;XI&w6|FL%qEPA)|9!s09g})xarRPjT_^RlojDJ0m=Yzv1|cLrQ`VQ)@3;mW7?Hn4iDp({E!ZOAo?h^YLwJk)iGQxjLImIwTAn zVSZ~2&YBOp@66!!;6-^WTCa2aU4lMdJtVKumt(~hZC(Q>`rP37Av^xG$Nw)|$NBQxiqRFKWqS?Gw&A5S<=2kd^r>CP3!QD? zW?Z$a{Xw0bUAbB|zvPE~Ho){Ra&~6PY1C1(*Pp7sh;0}CYVdboclRXNbLPe-df96! z+W9A7bNR8r)ZR_buaS<^`EZhXs?EPS{PJ4mb&SucfAi%t^VbQFvy@X&&b%^jQN}4b z+st!gd1hl#_PXe)$X({UL^h~Z5S*5OxJi2#q*IJNg4`8-X$^A;*CqdBjK2n2T>@Qf zxAJ1+Q7n&x{u&$#Ez zFju=>d9cu#E770$w!TQb#aYrG*wl6}^jpB$U`u(fd!!Y=7FZ@g?|c0PZI^MD&}9X8 z{1Bh?jV0v!UrSwsJYweR=UQkYgp8g+KjqNGwY*OrUVI7tEcDK zePhlQ0+s=c^Y3V*j5c)bF5Xq``ws4nEkp+c&$z+x5joka)z!gxAH`;gLOb)xNqZ9C z5?;suV;`^opAByAG`RU^8#ib2Zi%rE61X|T!Hw(`^;J(jSG8wgqW$I)n~%n`F5S#C zcQU3s8NcAyUd~4ucYy=*-OQJ4jD~#nCjgh~MvbQ?j@o#XcWbSzwsQV914lD(#Hcx2 zOU>EZqO5ycEx*B~+vZ2Cj|&faPi-~>mwX?WhE~y6r?a1NKKXC2!{;l&`!%;NIH)>R zs_R+>KAXW|EBpV}Jbt7}FuQwMMNgvlKhcl)SidC`r7sqMU&U)5=C@1Fn!CrLY3XaV zBicf?d|CMuYUfqj(RbM@S-huw6(8-$XH#x-3_7m?=H<@&ntNh^7yN?v^v+_QQQy;e zW(;$^5Z|o|Jjy?E`-}1XR_A#)Us`RJ@qCr@y!4T+N9-K9Z!$NEHC)f0)r#ee1%5uklw&O`HI@8!kPbiyuE6*cgvsaBaf-fnf|V=Wty#15X?LrP6v$ z3au{|tq-^HbvtbsI_I}b=b~%PLuxyhr^CC`&ULgC;d?6HWnbHRDj6SP$JcFVT+**U zWe>1xy9lxm3C!teizx2sV$#VUHk1Y#Z|FNP9U(&W$0$ZCEJF_Z+Q*RHfU3BE| z>N>w~S^Mzy4=I-}^_k^I=9~Mn@fRH#xq2P(a6OAWn6iO(*lH1cpEKC7CwDI>pAokP zThG3W9yBo>^Z|R-*HoW-VXzGQ-=@*L;K)LUKD7?>$;D>Q+}LG_LpRyXndnTk>Aq)j zfQF-cO9lsHdEeh!;Av;^VY?l7?gyordv7~04!1t2zGw*g`1@Nw8PEsM;@;$Tj*thD zN<;nHxnw->)NSqXw&S0XM2mY*q@A122sUFUyya~tFS(sRoJcz>?RIv0+bQg8=W0hD zNRI!?Y0n3~OTo914e+Vva}o178aXb$(eH)jd5(=}^Q`D{2y{8$v8BEA_*%+-V=p}} zf*zy9X|sr%xwN?0ivEnV3dmnn?uX|(^r!2Kp}iNNy+@EOF5PL}c)ZT-H2WOw*r#CX z`Q?$O)bp}9QxX2?t52Z)Np8%u_g(f2Vc(xku0y@qvv77WtoztBVeZ*upOK(L>75w1 z+t~$&n=J4q-#~4U-`&&~zqaF*Bi_KL!$!AZ?2BOv&Yi$1o_;$8e+O!^?DLVxNBpQJ zJ8A!XWG%9~i8(T{^n?w5@HCDV_XDwhC9^mY<@B`RBYXdw3 zPS&B1=U_)+x8_R65#!heek9{`26N{WOZx*^Kk=dW72C~iS@{BO%%(5eS;#vdH7Dl| zRl~=MvD!SB6O3}d@gi_K#kRQ=3mZcF)BMu8ee2bVJ(^sEnrxFdBU}CArDX@hyx-)$ z&m2Z)U_)JH{Do~D^WXt=jq)WngWpZ)Ea~Xc*ei--sSZmgK7;J>X^dkf<51jBdP{cl zU5v%pkoX^6^54yM-PhTR&=cb?%l0+avUj?6f%V2TYzbmzI`iGg3hCOH4)tE}?wj^G zebbiK9+V8!K4>rhEMj_DApUjjBQfV&*>hyu zglDS$&YIFdNpRp#`md9G^qS2xQG9siBg%)+yzlTQ*H^fdIrFZUXn$#)=8|<3)mr0h zytW0*^Fro%5%cVoA)2rAkc+~(tB1v}zw`9%?the>=l1Q&zXPq#`h$rn3{9**bihOH z;;TmHp#iw-SjVG}fX=3;p3Z4sW!(t8`Jf5$l+J|qg$+;dc4I!#MtlF`H=z-ISFA;H zTltspSzI-26}QjA4)tOGiqHJeMHo7l{h_t<`QSly*0rBe^d12>|L_g)3XLeoD>?@l zuL9P2*q8I6`C4dyiA7EtIhQv=KQ~+Pk6SsTvL=~V-v(#mRjr#uYG`Yu%}=sPV|C#A z*3k#s4(Ck~9DVlR?1RFei)Xjq%)fYWt$H)c?MUFa!ND)016Dq9MsO2)_pX7ukG%R8v^!U{!5Iw9 zkNfAp(#Bmiv}OB+CKq#yV$yg29Ny>Mjo(W4Irg#cF1!6v-j#o_;YwgJG9#zOH`j`f z!%yqVTfTM4(s386-(33L>GYeM)Sqe$_w{#OhW>V^>Cf;R@=5)b(_c63yt=gf!U&(~ z@KEF6!Q5X3o}e@A&@C&&=LW9^cT>54_a^8CS=d{bAKDC`hM^I~a1>kUME>ot7X5hM z7qJ;e)*Q7)EF6XGJK70t$DnIB$95ESsd{Gem)dL58{oyn48WsuZ9^lN59rV2<(asN zWn_?^(b=k+D`XJs75)^49!Ov?|ArF^yQ`h&h`F~ z&Ty5?`Pr*|@=kovxxR*bf%xBf`JpD?*b94R%U&3eWG_Uo^2Ixs_~IS4zIgk5UpzL? z7mv(goqe`19vF?>vCa#Aav}P(#uq<;+*VFgZ|^ALUB#ipW%=f;tB)G8lh!@20%nuX z`xoZB)wfl)L4;=_y;gkuGqiD--?ddHzdO97%B)XI_t`q%&Y>0!sJ^{qpk#&gGdg~a zReyFc(x~~*Z5fI`B|kv9(Xv5>=M{XH?l*K>1MX)q*Lt6J+G#`j`hId*BFL{OFjQk7 z35EdnfYyCQpXNHYhOR45-`qoPXdYxgxw1A=Q)SK|)Mq(x#92>Q+@u_xp%{B5Fjd~} z3vtGqv)6TCu$ov`^@dUS6+|1So4T)WDJj@yLL{pg0fd6*QMV&;Tgs3YT;FK#n(ta z{loL@*@yPKpt%V5qw|o9<wxa~tEic3@I`BnDkK5K9p(iC98; zm5C)h3rrEdmjajKr-~&oKXHxkPI7QGz9{X~05d-GHpS;OwqGhISaxcSy>`<~JJMs* zc}{+MHDjC3*e+&ludt`-653YWO?H^Z7D3L0Z`K&|joqbKs5?&ORh2no?Pjd(14CB$ zTEaI6jD6D0n8oj1jNeZU>ArOb;wTzB<4uemJfx1j!x{TO1GBzsPVNRi&B=#24ZZ+1(GrA9_c$P>av5cV!pq)6pN$!ZP4&YR4;u#^OFx8K@yC$s{bUjIf4J6`ZHJrf&+t5C!ThTDzDG>1 z%bHTHLkxyLPUUYt>vxIky8(6!9ca_7F;5X>= zyc~x=fhBQWeAu98@xi#?URxDkiM~-RMY^RPofD%^t=GtRGPVu#5&;I;H{X?AK;Oz8 z(3(%Wc8Aff+KbFVzo~skZ>epqpJ8LH$=^9C*ge@7-^2Yq=lkM&>3i=KU%Us~q-QF$ zN}u6M^nPtY@EQ13zlFyw&i9$aJobt4#|GGYO~6eAyo?rJR>^l|th4*`P6BT5)SYd| zL=H6{X|iSV*kHTMN2};}b|8K#yky@yGuWPnmx#^nuD0w=%XGvD7v_w+7n@({lUvl{l6Ly zE^Z9OONiSaj@dF}Cu3ZJel68HMlpCUb7aDXfkD{}W*kqPYGRD-yrVUC$%I!JhwR=G zWOc`6={BpUh&-V~;I`1;tqe`p4|kzDfKo4PbKz67UPi3c-IhL<2ZELs-@Q)e2jat zJzm6qs2`#_fQ*TI)B!w*zBD>Y>n%R^ajqs8sLYQY!+Y|p%M0~=(ZSNetQRdEcaS=P z@l{Jl<8$gPbtm5vJ+B!4SmmVI@~34q-xKq~yBgQajLXFE!RfEzJum)WV?ElNU!?e+ za;xq{=V|>yV|f&qHZv~e@GWBes(&rJI>xgx=KK-HwvzAdBUzu}`A3#6KG@Ej`Irxv zZ)8{Z`W;!u+-Yr5>rsjO%v)?^ApTe9nGWV^J$B0m*%F)AKjfXOF!LFKrlQU}k)A^) zSEb)`20z&?e^#EA!B2L}pZK1NpF2GG`MC!_2mIM~JyGoXkNmkw{!K?~Zbkds;NUIj z@Hfb-HgNKaZOaGB#6M>Qw^Mg6@|I(lylvYhemkzRMKQObeE^!q#T9<(bNE>6!?7v8ih);98+s}^g0;Z01bCM5+>PKETeU+v zW;u3e9dVQf;wY<#qcjpnY48OV{9>m{Z&l$hXe~R8o>SbqlCx(^ zq31kk}>MPp6}jw|C^5$J*{iY9%c^nMg(pf zfBz5O)b9oGN_0w9#Uf&fUC5?V#-ll@EU^wu)cYCQR(u#+ZJ2DVCA=%UQu%(B)2sv6 z(TCRQwYGp?@j>b8F~QN;L>r-vi1?rWdZt!Y#E`||$*h%I0nYkDmvde1y+Yo7SEaAw zquOMjb0_m6oP^J3eRiWq?mhrc*78SNYX5fbEi9&A*>6o;UkCnpCprfjXH9o*;yyBa zJ@=dWtKr=lf%pxHd%U}u>yPs{GZ3GVxQ^}jG}oTt?-kx@qg|~dJ^0mNt+D&EO&m!w z{tD$mYfP2)c)P5u7M;=9+k1LQV>mC-CU87QdoT30iQd>!z1q=#%{d9ZQ9+x+o#txh zzPu@$iSfH|j3(AnT{5|!-#LkQ>^*t8woZ59hQ?!WAuHeq>7{YRE|Q+}tUq=*HCg_Z z{l?xk$PQ|xxC}h=eBIJ*QSg<9IX4JT9&wQ4`$zehW0}S-u~^kGQYJKekr&ZpMu<+jD1|; z=|lUkZEV>17&)TB)Q6AVV%LWc@i}sQL%;g)(Wi^z?avj(W6u=DBaauw!<&oZ@aD5x zs}VnaP@5Y>rv^(GlsNU1Srb>j3)cty{BTnTpB)o?}b%^k8@mhfB`wWD1h{m?nqgL-m7}<{4Vud=9*#^imyk2$A?VMBG)5aR%HD21*&yX?+<>7wjb771oc>=Wg4Gi zI>|ofkR82EAJrzGRdElwa7KQElPLTq8XXM`^DZLhvAKr1Ra^GHrOsz&#k-!H#a#0& z&#xYBuZf60+}iAmS*sXRl-(v<@or=?wOSgr9;JL6<+7OHQ}~TsQ7ppzKA+#{3i%5< z*RXx6WzNfy9dsYIW_WT@T<|Oeo>I=8jcf-OlA*;#am6q>-?>fvrTs-w+L7$k{Cx!d zJy>0qq_6&`Gv1@*cH6SI44)V}ypca-@+aQwH*@V4{%H4r_xf_K)j^BJ;DL84p}mq} zD<10hKGVQ6tN7avOo{8r?f!u2hC63S35AT43s5gbJuJ; z7I!*x;(aHR41I^^yUKx!cyJ8=uAN*T?|b6U-?JC13w;pArtf0Td$H;7$Hxq}$^PQZ zTIRN@$k!GrFN%+5&dQjVfaPzKZ&k{iRav@+Z&fqeoY_#vIR%FsJl|VA-~DBYXEr48 z0M2KEhuQp<2jUNTug~GyRp6E}l+gED@4b24oA2BMmixT-YPq*08L#NO-PLQ(4wgyZ z8N7~0KYic$H9_#CK1PG5=-Q%q8Mum#qz&-%FT^vwI3r%Nruq+M!3MR%ec@1gvz|5% z`}@Y)HE|X(O4gErYYDQTtA0$dtA;jb^n+_n^>5D!HY2AET&y#?{?@W|ZG!W!AHU5H zs<)jQtZ~QcY4e-u+61>39j8rn`v=vFzZ6t#HWkjQbZtT><;Q80IoVSE;5h1UBA+xT z9ylkZYm@moDpwjkFGy~Im(}$c8;>rwF#efAHU7r)xY)7qi5{NySK0l}?|JT^aDQ&J)n+>H2d3Lz9@4Mms zbMXz;^y9anfjhIwGZc>zRT0i<0a&td)rJeV?U_A z)zhYj-cCu^<}CKx{P+Z9!bRz2!qLmq@b8uAl;gL#yZY7iyw;tr&AG&5j^E~%>OT|( zV}<1Jr_k_Q>DnZ2|NM!xIb>kkHh+<(&4}d>?oi$|dFvmJKHT&Q-@EvpK_70Zz9hX2 z+nl!TvP9eO9KY?|)%gR`%Ca?S+79oy^7*W?!;bzQoeADG)izV}(;*2b?2WAjF~ zv)+ZDlJ~Lg^Lu@W@bIE|K(T9&Z8el}Vz2Ku@oR_Qy=}iJINbPk$~aF4j>vHAN$g$O ze9^V|G$Uu>Gx~cTXYb}&#F-=bI3JM5BflXLpB`@E`;2P2b4PM4;VVay_gY5z3Qp{v4ii>-U0k>H~z5aSpGcu!|mh`U#)z!9p?n`6*Kzv-*Vw3cCd%`HI_&~ z=fiz9SV|3u7s-pQE*WLckb8&kwTD+dzsSns9Qmv^@KfX7=+l_zB&$kXdj?auQ zlQah67F%k`yB>p`X^(SK@Y0lLyfKndo^i%f;uoeqh}lox7auXPNcIfXw|sCQnzD1RSlhwRk$)Q@UTxPx<{rMI*2e5Ov%6Q_jtS{qJvY-slcL3Ux7)P% zvWI>`rapV$`69|s>6k-|pc49>3G6k*b!Xf87|HP#?Jf7U9Kx5;n%@qsmy9R&B3zmL zmQzgZW!Dz^#7DRBN6rgl5iWMaBiRp&%+gX!eV^M3@I+xftIMJHC+EUEpaKPR}dM7cGF52qG?^leZid-Z8R*s9V zX}=h`1168qNBlcqYxBfPqchOKGx39GlOv0~-Be#<=X4UM{;$6hM>K0A_L&yK`TMk+ zBmudS9CN#>kR zvo757pB`8=_X`{t+f14am+tek7&+OPu!iIy~jJ zf2r9E*+*ZZE74RhG|@|~!rr>OOPWljGg#`zc9B$fBDtU88@UqFynJ~n4e9)-YNJQi6&)qwv zF|c+sY!&_Ldnt4+`tRvI+$8)8&xfG*D!<}e$c!>zfJeOaTSA<|`iSuqejoYf=C1i$ zt_u)DDx+Wc+HGe#?QF=<&gn0cW0$6V$zajpFMeUmpj#a}=|}%YpY_qd&V#y={bGSU z;4dUUC?VVX_Cef3n~JBKvz4elqBGW3u-^JnnSDl_S6^P5vR^~(4`DB|+Sgt((V+Hi zY;^W*_}Q<)KDkE84ei(PLu2~wclK%2GT&Mg-2pv$`9x!_1+HicTsyHDT)5Kdp9#d$ zyyrc+{1P5O?gF&v=7soyM{-nhM>RLw71g8 zQRT7Q??|>*vR1u=b$G=gq1$cB?^kS8ZA-tHoKn_xUObZbl?BX!-Z{iOHZ4z&mukN& zd5m9x{=Kv$xgot2fJPQk2gHs6`_N~_Eu5RRrS!L_2KQATX=+<19cs1My5Y>=Omb~q z`6>LJ@6gKg;Lw#P6HKmopMLH|zxLKy6UORIo?S`H6 z6l71k^m~l<*VM9~?Gf_xjU5}F6hy}**}C5LuTRtdfmUZf)mZWeT9fxvMK`0zp23!T z8oTas=(8D}xSoBy*c4v7PWIda=*z65Ayal3`)ZQ0<>oxanAT#mJ>%GH$O_35J-8X(iZZQ_LC~eqd4mHMF(ZRmp2;_@I4jXr~pE)3%wmeAa;{+2<_Yj;+TA#s*u- z8kOv2v!2#Wj)P**3+)Trm=rS{eG-AH3#l>`b1E@=OdKPhBUw_<4NO*^)^^ zL*d7fp-%_mL1ghl4Dt zxQB)oB-&v8qpywKCI&zo_#bPk|CbXZ^|WyfZ3u?SVbrui_eFq-wV3CdpZvj_4NpDs z;e6`x?F3(?oU>3F_I-CD-_e0v77U+Z#!zkdi=5Z`Qv!Bxzgwz@Tx7@JJpEomzw?mi z;j7S1HPD;<3Fhf6%@_EKfIlF?Km^buTdriRDb)%{OqV>aOd{k3s zq7qzkPx(UYvAg$^f26$=3-kSrkr|8y*jV3cRGwmQ-C*cwNT>uG{UGzi9D^t3XE}JP zW6U#Xi|1uSf8f2}!2MN*1`=?Ai#64|shh#szU{4kXu!^^{p%&n-yGJ_SaTTzeUHUH z_rm$(56K$_Uc-0tiO#2A@LVc9Ll+Cs>!RNWUw|%W2I4>ez{!(nwee}j1Wp8_=xkR4 zPVzO<%=bpcxZPNZXTE&ZLXRD$i^+dD9% zb=gMmSZ|~acwhPBkp@GXA5>rB#4KwyA2v_^ka60&w{Jg}^liQayJ|YS@XDVnF!pTU z`Ny0Y;=EsVaEE{X@AYn+{gfYhbhY>WYmI-_7az)AOkD(Qu`FV_^O1Gb9XB=Ze8h|W z#A2;k#JBg`HG0;w56Xts)|cTt@!+VIKWi^4_HOF=UI%I{dw&i3I%=o*=iI=QrMDe4X9y{*Z5wf1=#(M|f`x;pS5(% z!G{@x3-_eMhnm`HS2=M84siP@W7`RRYn<-i2Jmk7qX2(9IhG;Bd!K z>Wsm=S5ETgVdl&1lS2>Wu}@BY2wwO4s_<>*`pP3S-fD->*IQW!V)VPw1DgxaBK#jO zEI-Ns%crgVbLHFNU->5K+D)FPQNcSiwA<~p`v~|k^Td8Ywd;;0oAC+X8l#Kz=zRLE zWxx4nq?!3HEGabevJ1Qz+Sv&G7hyFXttavUmPqnFYk$JhugU(`}$7W9Uuj{tns=Wqf=n`aF6gd(D)^_yc zHK%YLJzS4o_F;P;LLZi3KikjNZH*QO;$PB!P{v(Htu=dWef1Pn8&!LUwRcjoT>E3a zI#c^5UxRmzzI5dIg!9tN^UfvE-e-{M)8n5_rr+YudkTO4`Nh9orvHjIjwjQf18$c; zi>0fwt!*bP+f(>-nmd*hK5cQvk|f)c_|%r|hma|XE3feA99RDBKls`3TkOJ=g5SGN z1i#0Tf8P^+k0bx)d+o^Jb+xX$f1*R#C)L;qs*~x}HET2U9pBmCCNTTi z=u7>Zz4du_n7#F?Lh~;+Sl01&`CQu!%OFc&b{A%wp6#eZ_~i!!lQcR z+VAhqT`qG+ztsP@eQP&zQZ@T5WRz+b{fXKrwoPTP+q%y#Wj^(6E%T{8>vha$E4cHH z!-d_R&;GDiW`KQDI@pmPqPtYs|BHD{*Jmbtjn2^LYNt=H+=|g>qCapE!6p?9!of@6 zE4FmZg{qtC^168ZP1cfJ9_I|Ir1gXYt#-WN(j~GlPwR^p%qLz@i;P%??R=vZ@3@&g zU}f1Aowu+btbsN6I_&G^R=g1YUzWq!Ru2CcLN7gYOg+nOMbOU;+;eGX5OlNNH?U_k zJf^w=Va_4jcFibe3Egw zM$Qd{CX(sle`bJnM;cg91b_RWh5a5{D1sJ%=Z*J}HBWhH!M*++ zXhGMFT&c9>3i8p&6?ZJ|7;IYTKZXez#_&M8F`)a7N6)Ea_?;tjQsK#j)0_Sw50SzZP?~iAI ze`q@R@iC7j!&2v5acb$A)H%2QqC;NrL|&yJ0p2FzjXn;qKM4z3r-hiC0kdAZYxQ6{d{5@*E*Dx!{$Z8y(6s5q zZD-zUPf$NiV}GJ*t@$&+a&4M1Y7cKF`#M!OsZOw0{%4Z=E*uf^3_N;1fs+|&aB?zm z2v;ky?KWLI=E9Y!F%Z=*+?*ZSTkETkpR`*0wUzJVtX-S>g`1~wzpCO{a{f&`zQI0E zJai`cISnc2i9cuA=YnwV@#|yAp~OduJV&l4>yMV%1Aa#E6|P6%Pvw6qzcY*49>h4- zxb^B{$RFj87cg#pC%$HC)u|pJIen(KM})irvk!a-d&s{A%=lxGRm5BxkuMM6udT&j zYqH{<>+sRmUL0zWtmTF+giKl zPkBblA2GEX8glGf$M_b?TYQ*5)i+$I&w1waS=P6=+POi2tv&OpRWi?4ai?WHE1Z&} z@J6JD^(F1Ox3YuZ1+T)RTN@{gZ>l_nDB}$a_twbZBjcD8^z6r6|A_0}Iwo@*9JJ$0 zW{;Q8iyu3`szP%md8!&-sqb9uywkxu;)|DguY>p6@yk``&#pPlc}3uJJ2g2fBD37NiTF=5b2U|SbTf2_{TZFYT+o)< z{22UhK&HgzRaIzzfsI?v_fZWnhnC#jh<{UE&%8jK0w#%AM^ZkPHC{<+^E} zYh&ct6f=*P95v?xs9scLIDBpD_+mpH0*16Rr%4hYuhN!d9F^*d8y_U+5CWKPThP=oiFq0;J)({BQxROtFV)I1S(dU zdO?2T!oHR>fp>n9b)XF$Sq=~O@}2paTb@tMh;!TW1ONW!B{y&WFmK>159Lu~qYHY- z+hJ93<_vMjytaJKtI5x~v|{KoB^CD(e2Ipg?^J^tLb3EZ3DyW@w|JiF2=Xe1{7 zMtFu5KZibdP=~;3KPOnq-h6Lf{>ls7JS6+9Li5~){?GkO%5#5#KJB{ngW#FL$eIDH zT@P*fua4fPoDs+(=uKzk<&1!i;a|bhiH+XH+;&5|d!WO;;JF9-)EZ5p%nht5_S{b~Up2ON{Sjt-&IT_A>S!|A4k- z;9?ngV!vmb>d@~4clUAazJUezDyG{BeeMTmPjc-^a5l(S*f@Eh^~QbFfZqo%UdekO zlv(j0b9BEouyH@Ud1^r*KIdTXvz0@w13v2Bk&nB3!iqt`3B-|3y>{8bJ;=ko%-8$; zz1QFiJ_nt!A7Zz)U|4XW&V5Di*!6d)xtW5`O!ISQy64i&&nYR-T}`dSOnv92JnQO4 zo91mg2r~xh%iqE)#rRHcEZ5`=F!!=m{(h^?4+&d^Ilsum8?L-N;J59Sf~l%UmSV4L zMqbP~YM+T-M1Capgm_%CVFU7|o4f@1Ld%A6ZZmRV(J;OwfjWe)i^lcfR}K6R-7rc!RH~t@v3I*#LbD=aKcXfKfo$E^f>PdR=ak2 zPRoAieIYjBMCh)lp#IIGr4uh)i0{7)+0Z!9`ijXzp?z{9cUSMTb2rIj-Qw0ZoyZ=s zF#Smm^URj&{6cCy2DX}9SEt7KXrAfhnVmzhWx&-Aay(nX(@WsuWn@ds#G_3M;1NCZ zB4d7qF-KC?R9sJKOT>arZ@89fqS@2gny1?24<&MV_{DR}I=%pE2pBE)J+qr%vwv}DC&9?pBd43%~ zR`a|IKeltK#Ts$efg1F({70AWTw1f^>&bNXjSO`5v*-KEgidsxOJ`wloVE|~U1jUg;| z;7KbFDw#`QNZW@Qb3*2rK8`%x$G_e_79FP#cZ^}l8+`7Cj{JQha@$tP-zSgGcx!(R z@d_X7?acEc=GoN)uI$&Cz2p6323RW7;9vB(zXltIv8HQ#G-LI){bGi;#~-6@cT58r z&kc$zpmU^`Z_%FUJnV6 z@G+1l*~qjUWSaO>wP_C{n^l{(+o?@Uj=~$^a%@@4Ixq>Gd2MIGb3X8R&zCw-W7j90 za4z;hO>%wG@NbA2bYU~bkVWrW@yJ_@=^bR|+tBA4{{Dl%lAMa-$M{^3Qz5@o=Uppq zem1^e>>XsyTgaNX9sdv=W}nw9Ih>C?*4{;O1&~c0$intUVjyMgT|`b|;}CaX+~7pH zA-%oB*z|JQVv4`sY?~C11uW3=Z6PsEIR{LpdX=LU|?Cp@Q)>q1f7Cp^kN@ zg}Rzg5AAuJch=73_y6GcH~D=PzYFnu^GbK>RXv#}4*0cg``n5ag5OwsmpFVwZKt54%@Mp64}o&$iETjUp=*$55U4 z9RP0H&$1p|K>HnBf7r<8K3`hT6(HBc$h|P%`{;5^@J#4ZdQWv!$1%?fZXI`!*#0nY zAEmUXxS7dcCw|a@ezSF-v6&7yO*)e@D(&ZM%{<|H5PDDdAPS}VYcY!iX+QmF}Hb7s{n`-Pw8Miru7TVK%vSHuM z=ZW;;#Wm-(n)&?UiH!UF{^Nek9ryYD$1OS#-AHdo;Efo(aXcP~GM1Uts{LE}fLOs- zT-p>ZY0QaO4{#}-VdyG_KEKC!L|2Nt=iFL$Ft!A~VQ#eNs+Ahi9njP!%io4Q+O`XR z*;VAfw}Id7tFVo&?8afDdu%MtiPl|W=qIzMi+Ils%#rvF-|~&b`|z7=wRU{lPWox( zo;yF{oo?pZwtcgiZ}>)X8G0CZBJ(x5|9q)Fo@r-t|M_xxT)bv%nwng@X02=~D&D7x)m!O2+i)eMg%7?2)Opu(2MXU+ljyc3NZ!cvu#Q|JZxKiF@n# zdzbq$?so;^|K`2Fo_o#2jO6Ry_vPRU?EENpWDI>F+55oMPn#C;yy8ItQ~!jsp#L?q zU-^pmhk9e+#ew)gf=Bt2#l)wI@ezxOi4+qbDYkqST1V17{WI}#XaDLw#AH(UuZDp& z0<0YkIThN!s{O3FL+~+(HPoTMu073~`*E{sOlP%tW3&6frQ*}m#xwWxt#~%?yu#eC zCuVvrXNhXhXdZFG&f9a>?IPy*TWISeaQbmdt!kYkqITNXvgb){!DEtx4jn3{ z(FqSeN?b$swwc$~Y%{O#A}4*=D$IBA@36fh;&06z@P3uwZhZ7%_OJ@q;RmQm@Vi4z zVd_=5dnKcrq4CGjD-F{zm!bWXK92W07hf_DJHg~7q9+wI9&KPs&JDy@M(&pf?^_Rl zuE!Q9rs%cbKK!X|7rW>4Y+zl%3tJ+Ol6SSoGmm-i1mC{f#vRmr?c^-LJnWZz@ZL*a z@}20NPT|AxiHHa9Ry_E6`qy|n&lhi6_@wB6aL~oG-OP{9HyUf9OZh8c{>I&ryRM5^ z(XJz_?%hd@ElfL#+xP9u$Z2`!2aE}R>;$%#p+)7sy*$hwpW@5Jq`n+seifVi8ROLV zQoj{{5m*(=+)12cC%%7_IiT)Q%T8!{CphI?qysCMJNfWCq3su;ZTXpA8jw#e-u|ZY z>6kms^Blexdt)@ifyt>s{SoIKBzS2!^G*%wDd-UKivAgXsTpkc>rHrC_Sn#*dhxww zS@B-A2$z9S_3)7;g8P3s`xJWqq8_G{t@THczN*_#w#Am z#TL5;e%VF4@_l8Sh9?69c3daC?W12maJ0fpFS2j0nmW$%YgIR0Ybh~!Ka%H*@8F(Y z<5_yTxW%R2KmQoofmRgj7u`hiI6DV?M|r26ce{`ywVX|9pJ(ZB`FmiGO|FhF+JK$- z7`)6FrNde`GN%qdyKt!Hj9^er^>*;!!eGY;snz7r?|%YAJGjup3e7u@rOOOadcH6{<}Sg%(>d)d+|jM z=S2WNItUyBizPVgrv~3GJ`62a8ThUq83+l!C1rd9-?bC@1ioeG%g(m2vwhgvS)syn z{UHO>;=bJAM;BYh=a)?HWZds&TR&L{KZS20RsgOa{T6E&;6l8jKdt+}%lya}bMvX~ zx##>Z@UHfi|LK)mFHr+rF+Q(vD|zxT{-x|=ljrU1snB|yV%w@QB>qXr)|#P4p6i`* z-gyl_yx`h$JN7W!GO>pSWa?(%eT*1}kJ#j6z$!Us`{6prE7?yzF9W5W&xv$->-+#vm^yQ8@m+^LfQWd|u$lB)Z+npPCye|Snr|b_$ zt~W}wnN3nu|gRQw;$ms}ecJ^W`H4nz_ z%1yGnet`aztlx<)h@cBT_U8sa;yJHAxXXDaQcPbxqLtj>U(x&F?X+D7%)n56J1}@< zWQo16eD&T#=DaB10PHWGTfuY4jBR5bogz8ift(LJ&que@P95jTCM?PDd}F;6*-p$6LtJx4Gueig)H^ z#lwYJ;M?N|fkUm4hWQj9WOFvb%K8g~=QHpZ$mJMe{A^G0qNp6Ha^Jq#|e zccKqi@s3YSF31*(ca1*)yx$*%3_c8wu~nkjjcTvslK?V#aPXR9$>hPorR9>zLxR^$ z4ul>((+WK{#usXtloh%S`C-<>8HeB%eBa~RqxI0zN@#i&?Se1D(>$g@c&$6Y#2*q_jGWV!l=MvZ(Y?~VN_$a(|Edevzs9v%J*_=O+l7LfDC z_(e;aSDix<{tNRA_b$_>w&$xvq5quPoh| z0Z;j9@Fe(^Un1M0lQDJ8A;%K@XpLtNa-)El>lxrBdKi1@IgLl>^;sql)8(U!;X9o> z6;VA0#&7al;j@i5*)XTpb3iXMpJxVdb>Y0p=;egW|8LgjHJ?fOtN$uv68Wvq*|z%Yv=Qa|Xxd4w=itKqGvF5Ai{G=&nzL*K{55dz#kJ^D_J#00 z8hp3koQ&^_-Er9VMPeLJIOFiz7wtED?F&2avXS*egWqQy{LZrs%_-N_g>^sn*Jr_T zu?t%Yj_*1V9M8*u<7aFfe@{5(`>}95|Fh!w#$+7tUzeWu=cL2&jUF6h6DWW9FuWV} zSI47I({5gMJa$!o96$IOalFfgEd|GuP6WqSWx(;%HjYOM$9z8)j&nUYCg$+0c(8); z+BWYA(DSlnT>sD=PYOLBBu5}s&bBY};F?@`x8B5M^zY>*mw&S0AFr&sB|~3-PS@9z zzP_~AxwEi3e)n6wW}mb4NyH-`Q+c+tF?o#N&d~4nH2roq+WpGD&nF)}pFPXxN*7=+ zjeM`SDRK*aPsR?yS3Xo$ov78PdJT3gug^!2Uy+m_J;lzCZvJa;(!OUGU!HXq zfIFB5?so8{9Aeidd==kBu$y(!^KCoW&C7Q2m#N=@8T!3BO}~oIxVg)z?WWHECl96P zYvrSe)|!#KZru;HtFb6PoJ(Hz7l|Y4cR!p?J+Yi~TMvy2{yqZ?7o{0n`_#U%xq8CI zS?bt!WN3Gs({55;Hto7&5pDl2)7UCcZfxr_z>t$}Y?UWIwtF(P`#v?UjqYaN#^l*~ zGpeuQ9ouxqw!*bB5L~X=uj^`tn4)GIU%rd|O-}{=PJLklpIq#mbRU9^)Rxq9ntX2jYJOC$10g?Z4W@ zh5PltMlpok4Ey&+n!!_KA*Kig4ay3LY)m)R%|YrUa`yKEY^l06^S+(kJ{Wxo7j&A zXLsXUMc${rrE_*>6n|;rNPH>b=6(Dj5ALK}Gx<~M`}!$?&^Y|6@%U9|;a5$%z#rN} z&cBKI;TQiA+_>L=&-V`V+l~Rh>G}Q}zDt+AOx^!fed6W_e22Z4;k-b+{!m{mSn=Qh z*N(NuVy~|__1(-FtjYVetH>!TwW$9*5q}u{7OwFHZ^Y**$+e~krrzFM)}r$G%jd5k zWa7oEvMRLZw{bOg2lFR8dINZt?HnbpFS>mR{H~;q^{xNTxXrW1{!h^Df0JWiWIyj2 z*wIdkwg6=zVr()+1ftUCJBZ(V#)`L4q0IAYLO==prsLugApSYSCa;21nO z*2Sg0Hj}_*ql3$&wV5Tz0PHrct(vtD@Rg7Q%tr)YHy?Nl?t7ZoQ~FuDUG! z){Bf>urk1O%?ZGi@Yx-BbY8c2&RqDCWT<1)lwnJy#hYt3BW45 zBo%*u0A6>_HzVg2XE6J!m}l*)YW}!)ZaRFrG2sX?VNV>};PZ(LFrIP(Fs9;Da^A(E z=Gdjl*U>-iGp+c=(4^*lB)(}nT#h~lF7M0$XZO>N{{TIYlWm}NLYEe2if@QPRQAWE z_9f@S3!T7K4u8CA<+OEUD|A_$Neo^3pw+YBtsd|^@!GR&TLK$UwnQ^LlLapo@P4Vo zOOi#dK6Uh{!SCD*aQ{3F+y+))?!s>C#+EQT6k7c=u%_$pMQn;Nd7j?>F3iy1k58gM zckJbiTl1y7DB(o3*9A@nkRx{%^P5kOTsOW(F*$NE2Pf{@ujI>o;&?7jM0;L12W5cs zd&huN`vOFBE?y$w=p6?)sr_ap9s6rx>H2i_LIo8T#m+Lwt!omk`5U4~`qqvjy}!fPN?X z^4fa*g>5}Y8t+|c<+aUV-$JD?x5tmY^RAWCQ%fDfalRb#U2@wVJ#zQG7yEK)!`Cxr z$I6EkD-9Sp?3y!*-Jk2g;e|o{>&*8`+G~ae6+@ET*#!@|?T+PpJvlHM=OwhcoqU@` zJMLnvR?h--nrP|^wC$C@I^SRm^Pv3dUpSwt+w%h7^%;fM?fdo`_p|01@}+ohE8kt& zlO(_KZQg}v8~*F$<@ePYXr+z(6BDyw+$SS*B)=2;B!)vjx9x{UEly>@4@6 z*PeRqGuR8*5683@p7h!a$U^bE{ywYCFd_razJDy7$!37AJ16&-)9#!e&t@2X44dIE z|CXNTzkNKICff4Gtb;l>!_&;6mu6k~l4uTnESmc)Hp8nKU_Iw}ux7wtMwodTZb37Q+;PaR^19J0ku^Ij;1Dx-W^OBK&T{$Zq?A0~cuL+w0xDqx4^5vK|1N!k~ zZH5^c;C?a<+(t(Mv#Dc`o#X1pld%~lX6P??68)W&&45jPGB(2)rw)KG8O}9sOk&9~ z;OxeZPO=%m(TUm&ZojdAO_CppelKC%UiQ;gB*YitLX zKK_5&3}p@+UKlb;cl;skb(I}j>?`P5WNh#O*k=XUZrWQB*y`eGknnUBecQfqf~UTh4o|=s z13ovd&;?!ZK{oGIjFC13a{>0a^6jTzJKp##?a)9yaOD7(H|DWjanFQrpxh6HVxI|w$Cgw5rLct=R^HuDB`A339@hj~olOJdA*AqP1 zEiRs30v^*o`p6!)W%h|H-)|3ksGD~?&@m<0@wR`T-BN=dj`4guxM`4|g5T8O;MuL= z(SeMr$Ceqvxb%IjpZoyFp8c1EjdR&SE#ocd+~^iXWI@NYp+LPZ>@ zy?i{Bafp9)p2vg{;@#wWws*3RQ#>bLWKDLM``wLKl*7M|F-NL@oHR#{Z!X$X>@=5o z?u%F0bx~b<8?SN0YpHzsgEZqd{3sse`7p36M$xYi6QfA(V{nE({^>Y<$i~nZUuJyj zOL5VMo%PuqcTA?gcsj|`@ASapBqo2PtCP(FwW7P(j$^7lBv1qu5NHMo>;S4PsO*DKjSB+tC){h zx4zqNO);78hh~8FmSe!$<&6n|p9r`zHK^0{JvvFRC-?o~57W=hx17GcHoVSv(A=c! zTR8E9lk!B|kC}B`xPEl+%LcF5)=2! z>1ieIH=j7&cs|Gavf4V>=QjPwUH4A+DRyYb{bYyOaX+oOXs>K`%XtnQE)0TM{VVP# z-LVv0EAA(~x(S|g+a1q$#%#vfjcrrxQ`~R087uOY7^n8*=Cz#RwC@{63?16jdC0bm zwa-_PU9Yu@Czv1YpZK}+Dfph_yA5Z7b6;?0xBSFBgWbiuPw`#)(a4a&cC4*}J*Gy6 z3=YIUBwxVdpRt!~?ES`<5?@QV-#GFXHdf4vclB8D&Mw+|2R-pN^@ceAO8ep#Qm5ih z)!n$TB-l(I=StP)m{r1AUjCM8)Vm3y!AC zpV?jgpZ(i8)DE6{sLfDlZPVU2wtmR4}%Ww$ftY3Q;XnR4(^ijLYH7yc=JMmD+b>yFI4X)@!wHv)7AT zzQ8l=|6Vh}cV=);6F$c};NiLOdMh4jP(70GQ(q&irF}cH3K`K^OMlJT!JW)|cPrzI zuolADB{%h+p3^gW*5>0}<8PjgjEXUrU9I#nCJ@>?sVcPZg6dFr`LNL5$){06oU&*cwn zedu{~b~pX0p3|=+6ZkIt*?D@oW-pJ{knjV8-NG-p=xPNAJfj*fTKCz`TK_K(_0H|U zHc8ZlUj==!r@wm{us2%qJyWS4g&m{X18VQ@VglW7umIwsldz z%hWa~D!g|eaIdDef!0j-a4*yNbl!kV`}bhasIG~rnTH09XU2-40GW0R&IDNQdY-fA}7_)n3Mmb|v zzCbBCamsJ0WX!T})1BpEk2yL2!X5jD3~=Q-aC!6N#5;)`0qLRhZTmJ|pSFFQzR!jX zeeR)_b*kP=*QbjEALE~f-cPK-dCoUGPy2zqd&)WAEKXc4;g2&%-t*fo&O_HyBPmdb zJYgR@`f*J)I#K#xGE?&Bd32g>)7a;#$&We6-nbs-MYf0jx$W=4p8j-j-}##X_89GS zcrF6{*zMToYtCmM@O|)$_gaay=*Qtxt)o6ZBZIA@)`*3ptiZxiRs7a>@{x`{dnffG z3j;@eL%uVr;q!g=zNnk+&t7C zIcEa$T+(m!ujYK6a0b0_AgqnA)Qm86!_l=Pu{VpYxTyI#S?9BFLJsExwQ|P*blA1bL?+MhvRQV%g|@#fp|4_JLH?F zAGc3iuO#>B9goX@6Tq97|H6#twH5AKgLAGebw1s55jC%4U0!f%G4FTh*YDIbyny*l zQ_oQLP3#%Q@HlvU8hsBP?cq=RfTK^N;~q!XZDvf**t)rM33?iORm?j~&98`RY0d-h zS6T5EY?S$5B36Govix*wo9u%<%Y7Byb-s#Meb$uDW$18xv`*;yv^?vZg4keO%@C$2-&_&^v#~u^z0%HmMR_fqT)2o@=2Ey{k6#u42|_<4=U=Gp4$# zibYw}dtY8vQNyR|7Wj~#UGy2o-%HnTXKNt-^=#`ws}!D|4_vgN_hmciQ+=$*?s}d4 zm3!$sLf@6vfS!U9e&=U5UUuM)w=R2_d@bO)4BvMa_2;gj{@lJPL#aD=O6ano;B}q! zp4D^dC~NM3e5>)gfdd+6ekl;TyrL@9^UR>o{uc(@&#gl4G=h&?kRQNxnID)R03XYN zYb`jc!(M7&%uS4a9dct8HL{Qw#lW}sIcn8;;1}E)!(MoD0J!J|2l-z}#)0su*q-b# z*@CC~tp|-=$n|dU5Mlg^$(wh-cBO{pG9_S4dXZHpQVuBT1a zU)4UG`Owy-#C2vC3}~EGRM>bwZ9YJo`=<^KydM{Ak_1+ou z3BL9v;i?c^6}6n523O;St0{f+8Dl=%XWMhScLuluPy4{xtKh1IxF2=i+5*5GL0@c= zE$iUyPn=C)$JVl1-sZdHbUHg%`(sn>+&NAxOnTASa}7RYpWJhibmA$A^<(X`7M?Dp zPOoI{Sw$WiKfe&(C<@I4Pd(_I`Q$@}DvS<_dKeY7TR4+XZgq{Yq%<|IHRGXe=c<3oXM-G1XLr?GV z+;6G^p*JqC3O!wAg?@6mFZ3H=czP(dXyr^`niYEeYgTCYbYEycbN4#eMMIj4uQ|BC z6dBV1oTWwn#xiO`j-@7K>{Mz!jx1si33JbQ#1mSdZ`Gb54=+g0j`G6Ie2WjhAh>}~ z^DkPc9vBEsJFO}-y2uKR8RiRB1LNo+7Pk7#Q2X#%q3VGa_kE!#FjfO&7?~tFV*4yt z95nX0xt7(kiyV{BI%d(|1jgMnL~<3~f^B*tb2)+9YN>Pi{^8z?bNR=C9@*f` zr7t5BQs(lP$WoWby?o;GcmXx@KTjSPe~Zt>-{N!exA^@31&_Z*4*KW8t=%{)QuwqgFkPxF#_!l%3|hd(}5?uPNjvRV$C@3y^i z&~yD`&-D*I*WdSC-{-l$$9z|AT2@P^`L4CHtdkx%@14N@m!L zdR{|^-ir=BQP^D>b}}~BiP|ZWVdDREa^e%^syMRYBlF#s4F^2e-!tEBUhgsAZC>x@ zdnR7LhrN%fyq*snsqw4>t&aUZQTF@Lg#F(B05<4aE8f|JO@PfEX|Uq*t7Rj~cC`3j zJd79yaa_fEF5}t;bok@M5b9pzOd4q0_?6|hUnw8x?4lx%eO81mG|;xsZg%`i?6dpu ziSHveGs!Ayyn_18vU6mo&Guub&9ve@_y_y&o8I%-Jnosx;X;qRzi-8K0?v z->cB458wl=MPD^x3&BG@vWIwf|Nmw0UEr&#&b3-5bmiWg_dS)n}i?{ zQL%-VnKEsFAQ#b6OQ&M74G02q>Lr6S%s@M+X!RVmTE#lnzAqqF#dt$I?b~T*IG5ZH z0;#sVV|MPkIe8SmhpS{<5)^mTJ^{ln9U&qlgx8l1sdwjQB z@r!KV?GoU$Xbj=#^v_k8_=OBgRWc5*Q;j*m?t4hMHG-69Sx z1(WD0I_}4JygJ@ze8@@gWPA$w6%%{q2jcLJ8|S%aOPKfzoVt08i+#R;;_+lW_Q7!} zJPMy)Tpq~i{b>Z=n*50x*5KDN4_!!}u_!nhKfas(;qQm9gugF-t8Y5-EOiTI9 z86I43R?U!;W7%k~ZC(C7&;9jW5760V7IJh>=xo-xrs(Xln#9fl{+Tm>hHduN>#I&* z`+^41zmqwKYI5S@z}$Cc?zk$eAaG;u_Q3Ycf08rsP} zb)&;YN42Bc{}?$u)t!&_)g;jlSkiqet>W zbC^?8ZdtL*B-&6rs&S7ZBQbPRE4WoXK|8!{-a{9CR__4Eknfno!e#0M6)P5W+`*D4d+`ucaq!HnUuEfw(HtW zjfU~_-S2KJY=2M5pE>lrPjisod1C09e%@P8oo#;=&%RqSyx;yt&<)CEs~+z#dxD2@ zvr>JTY5C;P3T$29Xy@7{5sS7qq4zgIQ*3rez)Eyt$D`LmSK1rkw?CSLy~wrOn8$nI zRk_%FXw?p`BjBz>x(B+&p;rvr>8t|ny?Khc2c5Gfdtv!{f7%^j?|$A>tUT98zF-sY zH<=oy5&A96we)uUTI}ss*%V?rc=%>~wBu{ZgA^479>Vti1lxO}EkB-eG9QyV4vj%$Id!WuAHz5_9*sle(KvJtlE$I&XbfS_02?p{ z;Pk^E27dT10_>U-Qtr2aIU(hx14RXatI7ATG5P*{U@x$9d%zus538|DbI1XUuVs(6 z{}telfZs95>P%#G3^il2*@mxYxMvSuYWxrRnTt3B;4A#ud%qNf7Mg46se;e~b8Y*U zUoh9IkIV>N#`OT5rL$%ypeN8<$60Ha!spaObWWhg2i-Wp->kFsSDrbuGFg8ena^HZ z@*SxW;>@W9+84hK9LA6()n)7V*eLe0e0%1OEt#B`K;J7$5Bz7R|2vOxUP+7Kdk#IN zxzP#b&+h6NX(h~DkC9n>&fT@`l~b+Yxm7-sf36&czL_!`n_M59bFRgFk1yB-ugu|o zF1&nLelF{iUZBRQi}t%m!Bd~gfBxc+U;O;V7v?`d$CtOg8@Q^eU0sddn2e5{_ErZ2HRyPKLjwKO5jF&8cXfQT9_l!D@Vhz^T6J zofU!){ak7CleUdAYozi+WAi+As}la+OfuYl*# zb=^GwbLb=b{z~;M$g{3r<9YzC9ewt#bUM?1z(Zp*A8^R2uP>H9OWn_>6+6?8E>iwd zd)7pt?>*4G4Vk_V+-iS_;M*4kCSg}5y;HkG^wZecNhrY?HTb8Q)LZ0*n5VzJlQHD+ zoaRrpUrclb<6X^o*D&5`j2Br|9bMkLeLp>ib}E^>J(s!LxzPSnXkQ8L=R)J|DHlPD zIl;-${u9u?8{AFizWQtfC&_-xLYJst@CIKT>7adYzp^{>|Fkws>&Y}Xc~MsHiUVA+t)q^cjbEc z0NK|W!9V2QV7M`OVGM5l-p`<)ZRhMW)a|up8KEy2p0IVnWvAf@)%&H$K0NU~hbO%A zLvxIMOXia_htqzm#as-$fX&i;&j{(-YUtw3vsa#voPNfUQ{sd8w7KBxl71S-p|@zL z+(G-T&|^JgZ-QyPiWn$NR}18y!z`n+deVelpRMe_(7XhZKzg1X;t z&F#6UOBmZR#Bbf$~W5jv>a81!ibyc-W{`Xa$r;=uM_4m>QjG4ruqGgMEMb{^WZ$Vv?T z`x^bXAa^h0A7U?MGfh3w%kX$hP2$9}!0;M876aGlxnw<22{xiQ)Cz6d(BD5scORvm zsEGB4<40Qij?c3aU4NpUsL!bCSiM6@5FpD6vQAMtz}{BQTiJ5D{( zW@xt@}dLo|tF}6bQstvuXdLrh!M}*ffraq^h=uc_&MD%Iui3aJnopG}7)a|3k zQctu7xM(BB`@*|E)yE#{bzVm@ktfdg5#{$d8sHf@DjS?rHFOp1ns}% zD+#=EVNLM0FVqJA6P^@BQeN9pK?1{Og_I z)O55~zC#dFVu9b+uC+geO9MMin&Cz`LQgD=~wWXRP_)&Z! zIA*C%Xs%OpG#^~d^VA&Crl~nXkGXC2*Bp7q=C(IgZPlf=A^5_yuklqeKDXxRE_m6l zEwStM6{o=$rsk-a&!bMw(eXwLIEzCc1;5_sk6c<4{N!P2v85+~uoSMXyRtkQUpdBEd~ zXxHIkul*Z}ACGSZH+#_)=r8f1_)&Z)ejJJy#fRcYdoHs$6n1%1bxMs%S_|*4K8S(8 zvxjjuSS7~4c4buK!?xUZ4YuVcZ}d$+ zxyeeLSj+gEtV9jxAXruvHA|(Y24HUXpLX`&YhsMlna$?jQ1Bk-nG$c zO7`X*)zUB)U7s@7rRMrA&;2*K_VUR|=eOg0rqJ?IEBNM@e8E3{H6v)o2v3~w@Jz{U zD>(d%zTii{k`X)s&P(7a)iAvT-o!UIgF{oBgFd^pFtcS!VM)tL^wk^at3RaatCR4g zctyBhg>U)_y6XJGQsw0h{=UGRyJEHD3xNSVo=m}E2{fKzI4N=Qxz-H66CBj_%e7q5zzD5 z@kzDJg+2<7&}-2>RzmH(T$pX}E&cC?@8;F%~q6!hW{Jr~{N zZ@!y*k=e`{k2YZ{bPx#-#SrLG6*jf(?^EIie6cRjRNk$ zbc>54;pPN9ce0N5rB}eyV&*M0w$f~SoMjm${k2f`+)QbF6?I%C#if6dv1wd}ztNi- zm-5*fm&T`YX?z-&#;0*WU=H(+eC<%~sRDnrM4$9dA!O4(x^%@|h`^GFW{ndjrM zoR+DD!&;_&u_pMbuha%-ES(9BilI?S@U6zu;578+8-{mFdW-V`dph&PjSx+o8eD~4n6Sc8$9=|FV_UW{k7WQy5%#2 z)4yy5Km9dd@H5LZg5Ls`>FBSX`IiCTJm9-Lc+D5B;6HrD7rb_9Mz9CmT*L3;P0_I4 z*uqkiqw@ndHB>Eb4b=qB)r}1=rjDv4*ilg$tT+RlGfv51e=U_q_EZDb;vEnCBDs4; z;{k4sL%j49wNy`2OZCjhXZF`qjXygh_!Rs+<_wFPs>`UUnip&@yFB<5byZJOSM|)t zGlEfIf9g#62(ksv*?gPq^BL`X8KE`od;gchjpVoz{8W-pHa79h55@~!%yXvm!gTci zP`q%x7k3UXT>CzF;c0Zft82XS=<1roBk*(nLb~R|$m3sA*W82;_m|Q&e^EX90d$Sd zF_vGcJo#Tj2D|+po#Mz~8~6V5GDwWAwFGH0c#G<$jErW40_NKG>9%?9-{`r2gXeye z=l=Dc``3Byf7@K!^&M-?we8P*gX^*({Fz~_=}z}&Uh(YhmF~|d)*Q;82~vCTLHwD+ z)E^Ax&+KL`z!3h-ZMI)+`!oMw)6~9)E|p&WhG@E*>pg#6e@17}{nd0Ty7hzD*Dh+s zT%9W1$-buPR0nUieVyRNo1;_5c<|Qm|E1a2)9F-iJlZp_82`0|II+~kqdmqCAbvtG z9Y^O5@Ocjn^xsaWQ(gb%tmu<$Cisd_rGecZ5{I!b8YLGg?<55uJ{6Sc?WaY50O{wQwf=plb|J@-u9x|i?wqNg6hPk$62{c-$3e{nY;ZcFg+6hGBMY2>iLZ zp~KjXq4Pq_jl5h)>{evQZr75hB#sFa&%C6(P$Mx8F-_MBaJ7lpW;OoyR$`kq;4MIG zvliUlN^C90^2H^ADdeqAz<=Ht{bYR!aabvF4(kSq9Zo<8 zH)r({^=Znnq{hwz@h{wq2ExB^FB%Bmt6{s!-D4#I~@l;$-YNP7o)#bb6VbsMZT9Kbo4;PtwZdEx^Tb^8X)( zPOhA!(RC?R*q_xkiDI8^+au^r&C^HMVt0_mZuD6zIya8aZA0H4 zL*KTeZ(m2>+BI^vJyVU`@%vdDsCKZ^FZ;;*>_tcKv1{b?TbOIvkrO-dA?}sUD~->g z&R`h6=^M!YHO0ePrsGe18h_$5_!Dm~EW$n&2YXkP1gGLp^f2~5YUNZ{XXe=Iti*YR zzLu{Sr{$>!8K=gmv7UO!nO|g_s!Px~HQx6ct8BJv=mw0Daj1sQjIWOIF*Y;zNj|cK zxzCe~tE9N3rH47OH<=UrBXeRs^NJW-F=H#S?I7biF<0-E#Hsl)zM^6g=6_Q2?|;Q{ zMW9bgjUDhPZ!CLaeDXRsZ&wn!fjX&;s+H2dD)=bhGuP6GC871^+V;KInQOboPUjB{ z(2brNyN{;Jl2cbIucg-qr_|TM+ik1Sz35-n`>O8ugVxuXc}-7E ztXm&4uXbjz(y6hVQkxN+i!W1orbXSEU1xW^`l9|?yURY85$pn&bNHRQs{VSrOFl%s zT@c#(ME!5k!xb=4DeQsud&E4_p+~8#3>BdhswJy|dsRB8gLcct( z{lzp7*sIzNr|#|#To0hXV;leZ`vdjpjkICt?YHZPS5e>eZpn!L`r$D2ZN(pNZ-TCC zq464M481>0z1=sx^>&Oa!nmU7hZwq{^;*U`jd7y;+l~Ij0aWov6DEy=})#FSG9b2i=LF0ubhrbe$gelzuIuTZ0B5o1YCSMghqzqDm) zacN*GG5xft)jZ7_9`NSzT$|CmqBOV`d#E`qE8nTtqsC8jmx_;DWf!0FrPTS^bq=S` zWofQ%u(?P#mvy?iNK?n>jJ*{*sPPLQCZ`1+Oda1)zt<=0_=3cdsu@zQDm5NS&a=J( zAD?9GVYi+STdTR}6O7$`SG8KHvE-jO_S4N(niyq%dMsH?ENRD3CYA(V$(UlvTaNWj zcV#OI?{&Z9kt@C{SKP+E2yOnI%5^UB_bA_nsy^ob$<-{# zcJ?(}%5^*W2)(a8J=@>u+ZU!@*p~U4NA$_w@NsuvGW)*0|C#i?_WQqy{=EAO-Zs1@ zQR=rI(;2C;3y@*f zf0VHg%cx1b$@-o>8N=cS-|k!RW`)%}B7^=sIwr*)nf0Wp_NW6t&TEUjwTH@0C~xZ8 zm5xow2lP$W9y)uO!MAPjasOUs`1#o**vpJ@cP*m!kTcMq(_UueO5EB*Hk7-)VF}!%uCQ_T5q4;UA!5cpCb$!Kpi3lvZ~r zo1}fr7@s>fvu6+ODNd-Tmd+YQ-QhHTr;Qlzi#PPC=k}=X5FcfW`d%X+I6I!l+WIZ% z#%I{0{+Y$pqSJQg%H_SBF&vO@-9;>QinizYwa+ng%+!)*=UOf8)SMiGFQ4Y#FSsY) zW3*F))vozH4-D$t<aAN8L=}T?>ZHBWK8TUIGON23Ohd<1_A5X@N@^Q|&#a4Nd|wp*gHP4_FM!XyHe9hQbTf5<*?j6uMeRk_ zjvbQB=Rs%h^HbN2?`_Gl5-;Sj#%blbz3q(UDD_5eF1MWLO+67d;5pX7ur}q$z4Oc()K54TWeps&`~eK zpAXXac~(*2Wqx~w-=?m-x_1g=JfAU2->*X7&*`Ox0KaP+dzN=$V<-8o#1z__k^>$J zxYj!*0L}6je&D_6*`%`>1iQD))mp0FH z#;5(hUSz+olZ@|j#`ikoJD2gD$8+Z~zA23F7F{#Glm4QR&KtXrdbeEa-7Kfxt<&V$ za?Lp7wBhdE)yZ|)5c9(~-IS`s+p%H)GDtp6_Cz&7-t%L$FRI4AYO=Lp8)KhXnA?)a z81v28q5ULl!85tpE#uKAE%5#e=#m%VIps(S7<-{JcEwT!>`(L&;MAB4_`Q-bpU0S6 zc)pS`cYv2p>Cbv(pMHy=1ALu$o->GEVlA8G2O1pds<3O&p5eRJejSD1-Tk%*fNi-L zYc1%aT^F_kT)X%%5N3!RP1 z+M@-UUl?Wj%!^MNO`pIfnPbh4;S;TOEeBs?z*l;|{jw%~OFtd-QN~%S17I-qTQ&@% zfdRb2kKmsDSDpv=CXS(xOAJkbgE^ve>7PBYT7Jy^hq(VRaD0Kk%b>|ThbHo`sHL2K z68+axX(jHbj^H@@?L0rv@T_F+L?xdEmU;ht!jB#O3D=^9@G{B4 z#bIP3gTDRrJr-ET5^rVE<`!_X6+YPxpWFa#b-om`|L{^^$pDt^z%Y*fZ{WEx^go`y zGk9+dee2w@9Qw?s&usd%*C)AaZ|yU7+%-#M;j=GzV0_pEqi8f1I4jAG36D1s4-AG! z#nI__yc}45!k-6^4-1b!mRvh{{5#p`Z@<$doNjB?Z{#txyr$0v8&+2$>O(x|Id|0UgrfW*RATk1Ug>|o#n^oQR{Y|*2Un*PGQ~1Nq+0$w`nWC-g`NA=?mDU zZrYSRQ{5YTDo_6vbu-ia__(xnevXx>EZ~~wWlN{6o78)Z_g?3{%9ZE!Hc- zj?1r`)cX>Cz>D|+b5~92mA^HS9AF!LX%A?%Z~8)})Q|eu4=t|lQ7uJod=mMr&I&7` zaph*^2DEN`G;z=*;-IcNVg_g=-^-1C3$ogzhre zww~JKx&H&M2k4_Ra#G~HKDQ-JACHZinqGe(e`=1IJ3Fg??ks{FyLAU$_$I;JEECt6wO@R1t+z}LvVESF z=)mrDVt=CO6}^}3v-dihfUb}Za&re2+#k!maop3HcB&1Ky;fa-a!t}@((lh||CYHt zQ*YM==r@y-;a&&&sdJumF?);Pm$0u$i}pU7!yHu~XIZR5FUr=-k_zMpD(%il7%b$qQzw(QD_$JCHYQO*NqU@F~bXm3X zPsD#=^o{zir*CikJs!VB&w70(*%|dKo02lOPJ7BdhS77j-I(2bz~8_bD|w;&sS^sR zR>P+mmY3Q^D zmjMqx$!#X@$euCsNj@pMA*+h%F5@{j?|7cB z;T`22Tfn(3YZvv)n&|kl>Q0#F(De&k50E2=j#m#t$J-n_ikAB4&R?yfuKV4RBt6>} z*|h9vOwv;OQKa}i*G=h_FDhDyK5dJj4|+mA+&1QrsS9EbIWJy>e$F*>$edq-@BcjW zBsKijfZm$JZ*#Nq1FP|Wu3BO>*YMorl~?s@uTlN3{caU=bxwi57?*N-+raS+;CR(R ztGQz3s@_RfUclAkb`CwaKZkDScpd&}Ewf=1ynu|LC_j>2Mlf7k=*db%CuLxQwq0 zpY;2>6YzDfOYwDYldoH^HS_q?KcMEI1Nj)>>++84>&o7G`O14Oo|*wq>GM<0=QQVY z>R`O)%+J2MDNSdfuUFBJ%U?ls@Vg~t{rW=lqvQe~yMuGmk}*}S;q#-M5$f`^{JhY`=4l`pcnS@B{QGJNs_QIVs~S zW&YR7u=e@subO%2nc%<=e~iWEH#^8se5bWmqMk7e?|psQ!M?s6{&M-t3z|7V zcwGKetPVM>=lQwtIy$~@YerRc8*;VTKFdA4K{2{7J_cK+xHv|fxs1B3%^S{Y-uNxf zU4*ai+Ggv+w#lDAA!#o ztJaG)qkDbL&;EhB19(0hm>rDms1LU7z9`tScTRA0uEp65KF(^$2u>L75860(_SEv6 z1=jB9OP`Bd=RB}>^u@z2o?{sX&v&xc!{xb0_|4_FIn*d%_3vTCe7WY#Zwh+$Fk-&j>|w;`Qrf+N-;6)UyorsM)IE&;`vzN{pJR;1 zP&og&;|(%~p~hRQ@i4Xu{xqJ0z^k!k@~81=|0O?gjO9|p+zvYj)e%J-o_#P!Q6EC8R%=P25(MLsb;9by)d=3K^JuiPvzc)TQ8yjv>qf*3q zb^g#>_nq2_-g?!Id1UKbb=Km1J0?&Yvoftu>)j=fXB%H<8M68`vU-xb70I@KTSmKM zk#Aiuel$CA!&w_%Roza>H~V%P*6Cu& zk7<8c`z+5)_WsLY-#|b6qh^M>_|tiAaXydnr}K7Ilk-o@`*xO5+Y!zoPs}~tw`Ife zzZ{(*J^V3;4pDT)9*>N;^q6DpUD6(eM}pr`R^sWW+&Uje*E@PW!)foNr!6mi^V00+ zGl$%EY#Sz+dN#hj^Ua~&N9MfzFaC{HqSGg!&(YJTe%iM)f^QMUe~Y1`$2{(~pVSNH z>_o<)zJ5ht{cD{^;sZMU8vH*w+t7H){@Dq|h#H6c&&AJSGoG{i?PUa5V#JTF(4ki6 z2d|sh9L;HZMEeihG?-=EM$w?ehT$<6h7x=wr~mie{`ZT`xz}S))BX?toc4cUw_kXg z_HR5*``D>KxR&&gPu=(Nj2EwC0gZe<7XJ z`_pi>{8;NT8;{Oew$OMzwJ>^jJ?q7m9r|WfCN{*Dmzqa*UthE1insr?t;wIcx_#uH zUD8+XZ|mP~F!z_BKdyLt)VAGyeUIrGY&rVRA9_gVy3wcjC=(U6dBf+LTj71xwYMI! znp+u*bVe(%FOyyL%LgQ$!G=fifuH>8yF2ynVt$MG*FVza{f;jjrR_&N_i8!!{@Z*P zE>59K>-@Y+9>Y6c_qQ5;v(kQByY)|l*WNk2KFXz&`~rA=r$@$Iep3uMnEzLUPo=z- zaJ&>8-^Cf)8Ppy6t?0iuS{VVIscXK2Q+@Y?&nLlWqx>9jIh}J|?Qz)pLbCVk4V+gG z48|9JAO4@ok1(H+F8|7ri2e=_ywV7Kb=va~y(jvNLr>qcp{#j5yl}VS1;611#ewP1 zJZX5szgj$Tw}&Uxhja~f0y9R_o<)1+d_a7w2()th@Q<@jHd>|4W!PJ_q54SWx$HX0 zvd}^9$sRXl*7U6ATDd08%Z1O;{`}`ONBrMX-fW|3USO?Vlw5o#T_;&@tn0z_c?~;Cz@1WloADW%eXX9|r%^r(=Tl`UT7PgBYcfWqs33>PH zk6sHc8llNrbkG{Ek8)0VQ(myIDL06I-6wmdeMj7WXChlO7iTrsAzQax`p(X!`PO5` zepzQXE4LYGpq)k7uDuz}t+Z=w7;VaLiQ+G}{?=C&osa&gX08HwzcXrPqSjxN2*XdK zu{pZ8%o)oSj9K58x7fbYmHt`g`-+$C@2hiW8NRxzz`p;Tk#<{pXVuo)M0h@Sa1Huv zEjcW7N$#D6&94K8<^%4ae)tZ2{kumJcb|d1#jkJ0j_Q2Va2?~Q=kvF9B-7zW6lplyj!T^+V{No6`Jo)dJ2YiBol za!x(`HpbfUgr7g+#MKAsgFS|yV6WjPnyie~wf1*%7|H`=#AU-WYHF&=`Kz^j&yOai z5|4}j^y%`m>~)^;*HiOlr5WUtI1fWSAm3V_i}AH>yxDO;>2ICf8TLzKIK~*lj8Xov zH+OcBwR~Gvy;Redgp z-^0kPh0MyXNKRtpc2t|q2kzgc!R_;`bNDZQbKzZ;7T<`zC&8EWx#FDB z;LV<&(%K8H!N?3veLFSAIq1UR@Rq%< zouqyge4^(8lmGOE?wXj&fB)vf0v)L(GrIFS2PcubnyP+!y8hpL&~uW(Pip_)5PqfkS!UQTtuI*fwFk3yUZA`m&3Y5_0J-J{(wQ;|au6BO89kig>ezBS27l`}$^5*@3GB=;x#=T<%axOzGlLPAFDmRc zN_TqXME-^3!}a}Uac;=n^q)2Q>0RUbtLJZ<#5sr+%c4q4&0ltSZkxzOBK52VJ4Yl`H&oA&zM^G~}+HWN`(oQ)r=>7)971SSYWWluBvh{8< zuOmCL)S=z~8A4tUGf($!NwOXz48KgWYO1@;oasrpO+b&KL+Wo-lO)8jouCHJ;H~jD>Yf zwetTjKzHG5ji%l4v^#-z>$sl9^@UtVurX0;5Z0Dhw=eehKCV7StDQXJydviR$R$+p zeJtO{={vsDc)m~I`$WF~@HBA!YzVlt24O%yw42fo{d)V^beewd7@{A}DR^}Za5g~S zDEUhKiS6Y+(KpYbuT!7l(b-+sJMxLn-tN%Z<@ezGpznvF?e~iY(DoIVwyTC1+XtcT z+u-iQ(02SlocF_3Hw0WChPH*L>1Waq{d^eOe$zwS^z(n@PpW1*U4AuBSBsyQjm+dA zGiJ_%YgbXPfdLq|zxW1U{xm?@l zP}^tP$5_X6!zAnWW5Bx+9e9JS17A_S2o5rb(#1f)n3e^kJh8)d}91n5Rdm|24ncp)FUmh`)A$|uf;t;!Y=N~;cI2LXYoBDKo=7 zf2P&r!&kHOnj?vsYD`}9dsk7D(O5%U&}>2t^NJbG?bDcd=l2fq)5)jKmMW#KyGl8) z6u&;%Mpo$SL%#0^4&EOHPxAhL@~^87+_ey(D1YlS6Rj zZ!rJTo$r&MIoh0S`;q3)#;KhE7UiDtCtq2D&Y}HJY2I3Nx}KT{T{B0c?};+>OD1E^ zM8}kmW1g<2lzB8_dgva8{>k~Sq<^UxL1Q`Wj74*%?s!~yyN2+Gqu_H?zIFRd#VWv2 zF5L;d%BP_V9&TK1*Vg#pRr7s~jSI!Giu3Pg4(xz4FE5;>_`bj^`AWIRGXtLU=)HgO zjNyo*Ph?-`82gaqAMt@|JhXq)Qr28YM_Q7{hwZtFjL`PgZ|{^GYyPW|=h~3J_K|`| zKC8W##Y}3C$t#)l>08$=?Ukve8d(3z<)7Kq*EfA4xENz) zw=k#Oa_H?%EN}M*gb#b14u8?le?VK+vv$0F(Q9R#LA#y!jP{QF$m)GuZO){foR#B0 z^A_XmI=kkz;Jba(?J+p>05wyaS1y4+X)8oqWy~dob8LCLOnAzvN&LG<)?EAG*6UN( zhfngc{ut{DaIqX*AXodg^0{IRIbr2p7okHN(2ww~?stIOZ9KaQ`RVGjs0G0(KJ`^or8@c!ct`qI_4LXkN2PbsWl6o86Dob*ZTUm0jZ#~E(EO?cTX!}2a^s7E zuMJ*MvGx$W-tG>dRgs;ElQ+RE1@EU7G2Y}Z-*a+b@mv`gvfbgoiu%koZ^~Eba zcx{B|!Kc>4_2X1H2Cvek%B{P2eM)%6{v`359s1e(ZmS@qnpCxQlrsmfI=E$58D~KW zudUdd&N}!CyKCcBHS1ZS#&o;_Yb!pb?5*H6XHvO*0L;|ggAW_8s$sbsynYhAP6V%t z=T-AU?Py||gICF|@Fm>peD^YNeP@=nZwvW2#j^6h$ADkuOXZ(Q|FtekiThu1>30VG z$p604p`T)X_`gc~z9m>Yq)hQ4co zc_uIm-V=Gii!Ju#Df;0(#$3AKb@LN0yd!|u+m7Ik!2`cLOPnDzj5xR0eGN5yibHG6-`(xaZ zYaPiOcHlpA%Mb6;81^GGbMONvOK+`P+*|4MH6J6V74Q|bbc}7;^$Fgq_W62pr|v=C zeLWrIlsd?3=}gQe=-(T!Ti$!m=)=48jB;OFu3Oyu4DadLXZU`Pl^w$7j+kv~t+V5@ zgAMG+{(^~UTzLw_C-ESSMD!%{fWOT_hjv^%*xoESD3dO zo$#t^GTX7Ko%kAU)Og!AwtzP9*T_4Rkyq zR`j)5i-4Zh`od21ZdV<$Q8BaX7&wVbhv8#v@u6e5{tBaU~ReO;s9EmFV+MKrry)!~xBtZG4Y{mpb5|MH@QvdNE_ryigHi zsAIk@eGC^d2EEhD`;vFPvq9^exUK=lItND8YyiXb-%)2EUsQR%3h+hUp^1l<(Wm@} zF!*=-ET+$8gY;SH^eO+Vjed=-;k`SYeizg4GN)fxpZ{~7RN^Esj3Ydw039J}_A>AZIqzt>iM&hbZG|7sHZl6d_quP-GV-o6HTSPMVw^;fBO zMlq@As~quDz$}|{BYI2xxE0&giT=;aVlPN=ApR`OWBm%h7ZcwYHuJwJ(OBYW$r%7ki-dwRO zdHi@7-#v`qA|4vYc*gV29(0*}n-yG(-^<~pHgrOJy&a=R(6i~h{7);<545QoHnrbg zk6w^mz{`fm=CZFY?U?wSm^%4>R%pwR?-C!wJDLmE9&{HuvH1b|Z<}E2wGQ-Ih9@?k zCLPN2f<dAZBM+kS8>9B$=W@u|-Ij-Z3v>#RgIKH|Yaa5xJ*7?^3Zz0Ss; zY?bk|@xP=;#w7C_#8V@iXAJfZ&nl);ztz~_+yfg;PU8;Y4SNu&fcLQlUc9xT|0J_+ z|814@k3NzgbECswi{WGR#Ppw0b0C~;U~Hq|FZMY=@6=SGyQ&8DGhA7&u;n;=FgdQM ztt$1{Hj97qKYBLhnDxzhb97!%Udn#l;Tb;GxcZm}@g-t)KGtUV5`BlVtNM---!z?V zYOC50Wj8nBr&#zQE1=CvVD5ABP}(n`3;nEm=n3GqqY-_ulXsy{tsNT~9E%3h5x=AE zD{?_C{ngT6E&bId+TaWI(YaIf(%Hop?V-zO)S7+WOk4zyCiy)l^!$6alLcI~1JA0R zKIHL!VxnqdqS3(By$~B7fQHEJQp5XNTb>*G7BtlJ=`aeu4rHP$fPF@fb;Pj$9kc!{0a-b4Jj7~E|GcQ=7Mt>ae?PiN8Fb>wBCN#L%tf%bNg zBgyKID^qZH^I&5Zp9?nn>Dqx00p7Y9xA7eqhvLeNVZ=Z1Y7&3Bq3^t}wz7baw%}pm zL~+jp2e$2M@a4s8z+rqov__wGZ*}7z`E+@qiqqi`I6Kj6-CK$I<^%6*#HGKc4RE#% zJa!6y{E|-oxfuDN;yL-PeTTBD`t}RoX9UBCvP{17@!r0j zeRbS_NcTS$Y^%#+9@q+X?ZJnOb1#-l{@9oJQ$=2LQp;$5MmN5Op7?c5;>uj>_5-Swh8EiYJsRhC#%k6W@EPHI zl>0G$*FLX^PPxvVwN>m_ z^XV`!nSMRLo3_@&@>oP0ZSCI5~2 z3FRLW?ezc52dE}?yJA&?!jcP*=WffD)x5HP}jxIWio;XN6R)&o~ zir(L#*bJNW9DN-&dN0@5q);6GxUcZtvhN>zv@qCE5{ds zU&byN1*^u}FSj#N<@WpUyRCwd@;7R$%vTU!eK0AvoptEFdC1%*SNGX+`zh?W!SxWZ zcFcnx>VS0oM&~<_`~39g`VZRi5(cvwF%+`@tuRqF!p2v{Gc3y=38dd)=Xef zzdqn}a#jy3H+?6z_qWt4i~&xQ`v%So;M@!xKHe(_1|RRq2U$v6%LEr~N&jU!?PWOQ zcH4WF_9{H>`DxGIvpCPxnLe5O^sePQGk4qj_hyGSQ=jkh?oHSM`Ay_U5{o2P@TDuV;Ia$b+v+bQHYlGy}Pzby2bdQ;Qu*b z6`yvzr9LeB;Ip7M(eAG$r)oW>zO;s|)sCHRlaJMJ{|D)3us#mEedOSi6!>V{XUCH0 z;_g^ZRTv#(=MSvVLh%QB$G{Jq^YvYNjc20VZ+MIE!0jE|9Ufd4fNROS@&w*BN)NK$ zw7(u{WKH5h&SJFvDSN;5Nu0Hse*R*3r#)YFMLxB$hnUlAkPRv{zCha{&Ff{wH{j<= z_sb5Xz#R<9a=Z!`+nfy{W5r;_m7%+y)D!_<(PRr?bm76)K|OndfHDQCp1>I zv^?+fQfsvhljf^BrvO(Y@4=s~;6-xU4vyLzi4n2)?T2Pop)X!lo=JLJzu7sbj8GfC z+IXHH&pcjVoxiFNyVuuL91Pd_nLo-3?SkfQ_-2xwzC--pq~A+}eRa?PUDwx?kAI+N zm9x@1F=ExajOJtLl0~0q-b!=D{9efSu+}u!&8*s+Yt1lx!FXakTLup9fOdZLg7!CQ zMIS2uQ;eegxBOeJDN&5lT4%>7ciJ&Z3e84=^XN3*uLC#0E7>r)3;K~ytHUrkhP{qIu*E^rc*tVw1KDsI_3;NV-;eTT>4!x>PWhO8UKkIXiTt*0nHi zcblurZl``>oJW^sq01uRLOKzBI>OKvTtvb3O@9PewCUZSUHU8KyJV`wN+|d6o3)NS zm9X9w`E%nFGyjF1O|LIeeb}UI*$0cg5bT;T)tG3WZu$HR&F6}R_UFn)7pByTx%FY6 zW&dX77h9oC8-7tc{P+g-Ttl_tefDbh3s0*nyUndVx)?l-22bT#%qiwLwIk56e2^L{ z*~47qJP+8FPf&h-9M|?-crN-|bB%eS7lBJQI|5yz4qaAav+l!Yo#*ry;eGWNhAw^3 zL~Ypd;=tO4?|Sf^zW>S`_K-_o?<;(JYw?~hq81N5roC8(!k0T1jmx%OTKDP;{WrK% zjbCI6V?iE4V|}*7lnUx*<^m#9?g@A;9c<3iXJg}Q|Q`3U-EbD{V)m) z9xF-*zBg3=UV3}mIL;pK>f2LO*iZWwY60Gx_V$`zmv0NwXg?c#)x!trwTzbx8@k^I z2kEyuh2QKx@hN6V$JzGLA5!c$)c8~R(Oo~|3w?bM+#f?{Y=IVAp~W`Q!XsB+-0l4f z;QyA>;D7ZXc#sZFl|9dz>k?c4Jn_w>{`q{G{^`QDzFU%7i=1yzFtD{*DF1S^?7-eb-Q@pj?>CQZ@lHM2X*Gkkyq)ze^viE;LYjV)qg#8 znRb5Wb2=|AJ0(98d(`(t^w-GIYgVG^Hs3YJ(D6-+eb>Bz&1w3q@0zgl-J)I!8#76_DjILksT~ny*pS#a|_mKkU{s!Lpwyyt={hP*xzdNES?7POt zd1bBs>mM17Kj)in?HdOk7Ndi$4VBH=Zy$YbLTSwt&k=Lo=fgg{jO|;bn6{?s9(?LH z=<=hpt=pbiJhEA~vBK!RjL;JHDp*VnMwn+JuVFiXEnDb38$P3X7|%wzuQ|amziAzB z%}~!T{+NlglkM1RO4@J54x)ql`^8Dy5x&$!<0_6PxJP+qn8IvI8BCT(-T; zyaD*U6TZ5Ov6}g`MKy`Pfv50C6Ojq%&nY}7c%gSYuqdbajJ0)FJMT}R|Bm~}sq7$L zx3=x-pk0k8LcU3DO@=1o7L7wJEE)4Ls;fl#AYF-HXnO zA;+Tn`+Q&C_KRP?{*a3D|@@G{6G_VD|mN-YE=q|y%dLPuCNPOG%m@l>WOO6 zZNKDGd{oY!8{#|F2}R0$Rd;e;hUQut$O%wa&OSUDEgDa3tL3Po$x6f) zGVjCtG2T%QZ8LkyP;X>>2ct(B?*?FQ<+)AFlbd|g8sOL@pWEuWgmrXfirHpBBi8>% zZWWB;Pu7sBuf6oOn!bnw!o}oKDY38J-*f>w8-J|M%C;d0j|62O5clw9cZaah0j^|AOJL$iHKGr7t_sT>g zHE7lF^QD~Ao4K;_GdFJb9l3{kvHi=eYbN&fE!e!lI+D4v;xm6lHh;l7@_RO%-Q0FR zI&rM^@I4!*Hn%+_eslaCa=oIr_Lq76omAb>W#w+4gl?FGZg`%3-ww0yTLZemLT{!9x!=oBGY9+7UhOBS4vbMK6@r3=n zUEAS(UVM?Hvs(+9dft@q(%|BST9}`MH_P+S#m}m=dd)}$Fqe^wlf8$Q(pLw`Rr%qA zrUxwJ)2M&RannBcd(9*He2)$Xr_e=n=SFW=IC{GTJPucTF09V{c=~szK4hu`x6X2K zL3ZH{8hU!_(aC2IWkWd&@pk-x$Gr71g4>L-l5x_%n}>KBe%{Rf6BjRG-V*(%I5h(N zMwej|W8fq@0lJQY?~HzPo-s7d3_VAkWR!77MlsF_wp^-x`L5}GOr?(-pS}6jaW_Bs zd*&RTXdB-2NLDGeV?68D@ub9(@R8^lUt{~N{|mgh_?R;Ye~Y)ZuZ;NH8l(Ny&TXC^ zral-R_Q8))^r!a35Z|gEqfvFj@VM;AH>u6h^;meA?};+vUsET{o*O>SI4ci3b;68I zH6s~+=-Y{Xwe49yJ#Aj>>+=W0w55L4=h{o(-nr=<_Oq~NGz*_$ckl2ZaY`ed7Oic*MoWVY6Pr6F+DzBCznK=xc=9<{xcETM-2I!}{_J=vx&BeI4ZTg>x#r-@u-#*b zCzP)`fIsNX5zk+YEWrD+efUZE{%JMVPM%i0Im?9sU8D7)>ffzj37RuClJ&vzPhySa zQ`e}*F()2p{l=^k=6+V#_qXO*`&7@;wp#Ut?B!8s;qUq4tMRk6AK^~m?!4B1|ArCF zslw+j?BjtsQddL!=@k~{XDqP8WrJ2`9+v+#2$R$CD>9IC!(9;;-d}VM(qeU=3b47J8w`O#*8Em)y^+d zo@HMQWVo7s#yz}w7k*#7hW5jA!C5u)i=rayqPgmzjb8Gx+{8@_x0@) zZJVGA^CHBG%7gpU`u2x1RC{QA6o2RhwHsPn@D88Kng5B;Q~XILjE}*3hyL0vctCu< z{p(5H)9Ti3O`uM05P9$Ce^b+V8vf^OnX$u?{J(Xo~d0f0H*+I@VU-MqFBj0+DBM0*Dvp5^| z3dsR@+ykA+R~wz+l^wNN3y%XYHjeln-@W|6)aK3wk_+k{X;Z&T9%c;k+<)`j>Ez?7 zs|S~l&kiCVnr}EveY1DJHSMp6Ul07g_(tGg!hF;i#-_Z^)qJ;Dv!pr=>XMOh`>fZb zUuoAOQ=4(50lYlOGnuTnxq`p_s%s;Lk>1uG9zO0ZrZ3o(T+t{;%==S;7@z%7`rzm)$Y|Z?)HV)rupEAcZ}_;O!D4} zuR%9_GR38u6Kk7~?ORww+wfGS%@f}qj7`jff zvuWMvUe!jdaC`~TM|_ciFOif5+wRl?mw^F#<)ru#^}PrD4Zjl4RXcoK0qt&d`52f* zFY$45zXSW7+n_@ef8ytx`TYJMbWzQ%X!G$j+Njp{2Sf0q)&Qi>e<^>XJ(4@I<2ob9 zPfo85{I{b2bYH*A4r*<57`@m5eOBUk`lx67GBTvxi1JFZiL&9c-C8r#4sJW3k>;2+ zzpA{L&YyT4JRj!Fotu#5e4qNvk5?ckWz^ws<2U&u-O#<(%5A}}x6DLO&MeHOtUDth zT`Za^4)gjlUHB45(ZPa4ehm0FdlSVffCu z;t^`3`}ic+#kz^2_!2&k-O5=|JE&_5fUm9gvwBB+WVf!d+qg5)^jbq^k@26N0=K_} z4%OJ!TKr5~M<#0~Zv86$@M`e22Kudqe%Df$v5C5jt<(+$Y7)n8Wv}ra&>23L&!rr7 z1b#5{$jF=9-gn<`JII-dop@LCcj0+_!dK<^S_g!C_(!rSzvunH<*ri}T*u*qXW=)! ztFZ{~VS{$##ukPdXK70h# zA327WM&!1!un0W(17X1lj9Tw4S?#WXZzouZub108SacGNq;u8&N_;1^ z-wB^q(B3TmTJf0_ckK81$MXl@KkWD#?AQ3o z<`=&>(1!M}k9a66crz>Xmk2(h3?EXw79evPkt1dd-bg5H-4uYC_ev_ z&m;V4eeuhD4)DczU;1@%I_}D95`*pU@YlfC!T(`=ojC};ZgcQ;3!gVS_}X+De2sV? z`0A;1>dhu-emFB_emHz9c;5{#-HZKa-hcN6?C&^g7P*EzV_W(-?>>aSXs3?W&beor zd9p5eVNd4PxPPp*Ae6baCGO8}S^ZAaj@3nZ+gDo!f$wv#kKgx>VLe`hl{f}3ch{pE zu0^jtL|^yYeYWkj`;YEKkL*CF0#B=np)*6QZ{OMV5O)85oo%dgH3Ack>DFcOkRkyQim2PqnQ+Y5PUBO`Boh7Jqb^G3Lenhv1KFy}xOrFZN3%^Ib-vs{Y(482@R7!twIte0otkRNKW)d>)Au~uns4`` zcE!KZ%q=a^74!ql9q?Z#@O9u%*Z6V*;Z2N*-$lz6+M@>eW@skY z18j=cF2V=0E6=m$a)3>q!2S>(TkQJpitkt9JH1;nFy5V&&E61JHnB}kJceC9%I8IV zUSx6R9s0@EFWKAkm>aCdb}RnrK!&3A$VeT$S&gjc!1wTVCwid^ejd%c;^$6y*=xTO z=h(7w2es+O&zmM0&4{0i@1!|YZ!Xs4Y^#$wTjiG|lgiu5R?AMix!FqO!nV_POsaZ@ z->bHWPvuWeaIMcaJ_q0wITJp=osQ2k&O^tKH2HJCZEx~EiM{cs*_*Arzr#wj?@|1d z6K?^}>>0I>y&?AX5x-dYK~em^ImFYf`wp;K&ygz&^Of(bi%#J4Z0z`8k+OY8LYXHKGXAO(iZi}$ZJ_B z#QIjl|Ncy4PfdPVbtumF_mf|~NBL#XJL&m|O!5)Slve{D@y#~I?0x=f=zA4^9oX`V zO$?tGe}G(5*XZY2qvh+Fz@7m6p;29hV)z_m%f-t&7r6_)G8vl~z}CfQSc#^ry!e#| zx9?KSubh4qzcPo~p$66<9R!cIFP)4@yZ?@u6#Vt$ZkmHT>|Hm1%Jp{7Bks`Jvr2<| zd~v;>ANuW3?^dE0(QzhEv;v;zS>@*Uuy1 z-EMe{u|*lHYATg4(b^<8Z!`)1zL@jbu7pm)#eU%1MBcrNc6EN^A#8K_TGqnAH`+g@ zkh%TW;j#OC{+3R3$pk(XuO@YmfBWn3;*aocAe&(c4AM%Cw9PuRmx>4Cm!iw-upc+#qn?i~?!gvMeYa`H z)M9J0q{eiBmu0&R@+*o4?Y$tUFg5g{~3!Q**i-fGZ3SGUt4|=7w~3nC4i0$Xo>Z@FRCI zba@53K=}x__NEBkDc?E*Zz(qsL(jIZkUp?_x;W3W4IS_!+IaT*?A^~?|IF@A=&!Z8 zL2^x9@ZtC@$uD+lwQPc;^D5va`0^NSZeLZ|o5MV^+SI#xNB!t|wQc%Fp4G2xsq(KO z@>y}la~1mh((mT)zU;ft?=JHd#)IUa&b#gty=7UP3r{Zp*s<$hJ9pJ5dK)rx;uU=F z<8vE1WV6o`dgT~8rN+u>S?bFn=VLiJpPcQgFO1|^iD&`1D?xULTZxX5jA4|O=q|Go z-@<26&PzTz`N;UTMBf5xltw7Vx)I*d`lUng`+jVF1)q}r8t&h~zBT>*Sl|7}C5oXu z{V2wX(vR`?!Ik{|fBm@A7i~+HZo=k7!Jp<4mMSj{z7$79!JXih|K1ABiXqgNsfnST zNCo%6t73*9oNepu0kQ<0FTB;=W7(bqzFGA?z#`jy7uTx&`WM|pr))95DX&xz+HHO- z2sN8~_TE|FGuMUuo*PlQK)%<=H2$wc4&}S*pIyJ;GxgxB z&XN!Nk^cS7WJh9~7`Gkgzp}hy7JQR!?DZbv{Cw>6MtG+oGc#UyhPB{a`4^iSdf&~t ztXK0LTTINqIqqlehw@Qv0ovMXx2ZbG$a?TdA3D4Czj?Pk08Y0u2b#H|KgQ^s2d@*O zC^uU{oRP_Ik@YtHDzKZaGiVpyi#5PgO*M(9(20uOS>w|33^C3##3&ut(k?#uFVu(H zqmP65B&xTReLsZkg&9XRJYA>jh2pUs6U%~ECsxndF8yd?^+muApD3SZ;!OBt&^QyA z#dq!E7mW>mnGV0092-7*7(NrfDaUMLba={}&owct-a}SQtV&yw6K^~`n)ccm&wd*WMn$ zr<{i3Q{^N(Xlu~;l)g-SI-mA|)5NE%YZ5_?kR8JdoZJ2(~?_OU1DRn=J zH}~=RD1VRer~0gi`LpAo%+P~e-_74(GWcoMQG0FB7)J)Z@z115L&raxsg>A+4qSr$ zXWgL_mlo|(<3^E?wp0#;ReRc^)mLHZ)DGTKQ(cgW?r)C5#pi|-q^?+ zH+f?t#X*XP6c-T>y|SuIIv_6|2j}|k>V#x`l!*^k7WyJOT)H6wJ*6My^lHCeJbJ^9A^0!1Qo;lZT7a8=nJI-x)MiLkQrFP-%ly+y(?i$+V zIqePB!Tb-r9Qv8o@UEc`p4S?R^!RGXSc&!q^Ml5~2j%~{oK5{Kc%I0g{QI)3?0ELUt;AKfo?Z%_?qobi z;hl|x#8tP0BZHp{;Qih33-+mtRu zecqU$9k`5-4cr>b8a>Ncq|eOQ)}xEiLv8aIAJ48ZZSgzzqTDlaiS!-%>LhqhX&d}_ z+xlDHHTq}}U;pDb<7?Bu@F*M#Pdan7&%sj+Jl*Z&1p3GaJckV1>w8%PnvzGz2`#5y z%wGS;ew4bVy;aUt!Srf)S*l)rV2J#jUGI>rmz6EX zXM08QPXTy1Kn|&q8lgtwAkBF$W~?*8hx7pQ^Vnj3v#jyWXa5KF#rRrd;E&h@yADt5 zV(!B?Wq%KN!5=zH?~NkIk(9aHy!ED!e+m4I5^t(*#9lYAT58pnES0>CG&;i_(sC*GcDdX_<9$ruEV02iTw3(1Et)TW%2kbBJ5K z_H2`Ves|s=^{{R{wI#=TO!Eg3;5rEIh4YPE+pyUFat5-GP2X1!ALbP11js`lSy`AH zP;O}hdC~^nmtD~uep)?f3ceQ!e=EQ@vN52h4Sj`fbDqx(U7qqRHEqa;;Plpm%64cz zpcUKX*0g0vH%UHc&JsBIvH%i{edkU#crg zfxl8PAx{Q=#^KgGig(;NP&u73su8aAjwdH{_E7IuBJ=1L`M-7KP@j8y_G{H=%#QDc zk5$K@xV8fsQG2y#%zoeXHwQ-4{G)u{&!0uC1-~k`sV3G^y?U$y+|)ZYIOs3QoX^RX z&6YmkdHvpRe@!gm*3+iyiqqFl+3(cCyXW(#+MU^ae$Mc6PUr$Y4G(+s1;8wRexWf{ zpR|{8{y}=YRA=*dmpP+!WHMet&ugEF*dpxmY599jui~S>6o2ns0PY&#k450m(J2M+ z)D217-Ou0ulJe}}{H@%Ec-+h17ek*gG#kj@_E_NU7UOpU?2N{rpbVh{2=M89(59DBc`52ygzqhc^l5``2IOF|X)dUoLwWLtnLn zgFf`xTtlBsd?H&9317+)b}&voqi6euuwOaiA=xkVTswG>4icSYzoG-|7xm`z)1Qa$ zj2*MrrDm-g+q>ELy1sZfa9y!(S?_cF-NX7h<*EGmzooQ2n1ABtBITdl$GaMb*PiKn zwZX5wU(6>4!>?!F?}epl@~{0$p>xP%-^~6Y_^;{neVW6K&9xH0cpJa{j0??tpXyle zM7K8ZeKI;w{O|bTDg6HkI8q);aky$(#q%ysBk=D3W$j(yqpr^U|Lk-|x(KhTx^UdA;T}lker6=RCLbJkNQ~a}4i`2c5a$ z*Xhs0TTRfU4%u6|efj!Ibna~C`p8)LocF}TE6}y#&0C}snb)GwK<}u(LxY2F1$kb1 zsm092tT~cXT;A0J99#1DuHV9S?b!<~E$osH_HpOlWuiS}GGlYbl+aCi$?=Hy7pFW6 zkL2|*_rk}@S5lsVJNFi9Jn&)@bZTafNzT1fjh;Q9Jf_N`{p?f9j{!%I@O?fxEKTPT z;QUbK(?P)(|>r@LGWq01cT|QdUei;rd`QC#Mw5FpRU9B7BE*;gSJYCsZlF5bS>AuWowC}Rj zdA@*=$;)_7IBds;xei_1>d$gwZrQ6j%dTb%*IHPgu=h7)ujU+|noGD=X=NGS)czy! zrs7$mr5#7%Ja)+|bQEtk!=sY>^X&nsOUS)zZ`u3kG}aPm7&P>>b?fJPqNzvrUtzzZ zH%4P>6~j|C9v$oE-*r%5sUNihIJsh=t_Y|G6uYp1U| z_vJ6SISleswRY~~ozMAL7v}dS!-GzJtkP_gH_&#m_0$wT(ksgS{w{u^o)v#->`nN( z?i>>}`q~%n4X=ow>7qr}z)AIBhQH_iFZUMt^Udg(S;@KL=h6qekdImPakDd5 zn0g*Ie!TnfPIB&%cbuI=eeuIXY`V2kBd~=UfubWq{!|$`#%ma(!2vO#G04La>b|!3 z_r*n*<={FAzsBYZ?_dmV)G;*g452=;#?AO!sB74oT|&MO^*RTY#5;$U#E-65lsOxztfUV-+%@mUithi{k#qK9d(0 zx-NckY1@uYD+Dg_RY4D9{gLS9jL{vR#@^0&-7&f}SG^cBcTMt+FA!e%vD)CwhDpT7 znWK9B-r2(mJWc3j*=Lb|BS(aIqHKZ-#0&oh4*-|e1`PhORn7sz3U+Pk1kN4z(V!nX{TqY!iE>_35BpZ~IB!`5o=V&?CH; z$Su)W;E#6XA;Pm!-cer066ye|t>!18)$eF0#&f`$P3)w-9DbS)U+-aEd=cy7737iZ zvvb0mw$aaH^mB-QxNqtfBVUJ^ekX_Qs7_LPhsGnBaQ{Gm` zHu^b4Ka5kda#V3My+@z2KV(N5IDw;_I3Ao`0B!~;ghfL~YsM1SG_2xAZ*i2mXe z_FtU5?Ns;7rp@p`9XzlBy4S)7Z$k5R@WO-8{|)$IiB12=LGauI4_xczfjVfn8QLAR zc_3PYp1vR2y!IhHz_U@_YvR4PsK;P%TSGhd)6Q!i9@xN`YN5@W&}JPpd=MUZ!{&i# zHGMSFS0(*$|6Cq`e!Oq<0Q^8ZULJV9kHZ7B*Ir3K)%4St$^)W__`s!q3otYTgLpuC zquA=xnTs#@xw~%f`U!b4SHKT-zJ8s{(Zj!GerN7KT)^*bzJ|Y6plcRh)Q9uXdW8r2 zQgyIbKNE9K(a$=s^M{->Ae|EN#<^tA=zI?CKj92R#k~5nkM|tA*WG`zxumF(I<$%{ z=+Dj<@Z0$U{FHRy zA01s7Ev8n-HPp?Z)~3GaQEOA*-=Wr~@-$`ZP?s0k%n0AA^;_g8T27nuX_I?uOY^Ge z+I$GUh`|@d>?P9r$(=kqi)YKyp54Q<$a4!cG&}^4wL{yEyP<12be%8y=Y?0RF8Cta zm_ZvieatrQq>Wj$QD(Ps7i~mKdAEW#rV^KjHf_6Pw`3dJ!K@)Y9gJ)JpqP1d6FiVU z7i_(WxnOEA9xFu_um!XhAUj{{ql&+nIKiaky!p$N=j|H1iu>yMX5>}*t8KTiRzck0 zHsS_(_@oy7+H<6IUm!d~xw|}{=ywM1f@iZnW%}0{OVoe|*AaA`cwc&aAhl4vIMaB$ zV*$=n<Kbg*YQk@8oT<`a1FX>pW|U@b6Ee2 zR@LA&76?Vl1Jq#lho0!*{8iTRvRS9eQH>wqkAgEZ24Hm8*COwvt)DT5`{9?Ac?cM^ zuUGl<8ef$0H{BGB@7KFaXzOP9=N9&q|l)u>jaoio-eypr4<;q81fEcwl2-|_k6cd*AdNyx9?_D>e7 z{-Zz5KWRfw-$rh`ZNKv;ldPF_#C1EqXw6)JUB4>R8efb(TZR7Wzy@x9-+mr_Y2Il$ zZN;08Lkrp|IBSLSiG4}-hN;dmwDQqL^lozIppDk{&6$%Em_wl32Fn_7_V_jRXUnbq zPcZJYh1PzZ^)(E>77pZ_q{~C|Kb*%l^4s=&!p3s`Z$@84pyw&p`=jtt6a5CMFERp} zn7V-Ql5!7X6Nse+t&=s>eQSFV+QUcEMGGCBbQ?MP|kMe#o_HRwY z1LO?!vd_hxiku@O9q8D(W<6cGX1&5QnIqE2-B!(b(J_)|?Hjp1(~3{awc?Y!^}onx z8R_S`k3B8fR(z_D_Sez3=quW4ee`Yc@;8~RPmr%NSiiN;DnFNa6g25YepA+&V9j1~Bo6WaBbN~8M6b6&%T~^`YiF;l z&iTlNrWWw|GKnGOvagl4p9i)VfUTAIh32MwYPHJ-*$qt3IWW2B3ml~mwqV-Fdn19V z0XS0h0%KaHG4Xy+WBQx(zIRL=uTftZn8pCpXkbcd4}7(OuXga|t#AAE+vE-NUIBC% z?$E)l3;n#)$4J@~jKmC5+XTj&1tagL!#M5T?yH|lgONP~LppfpGVGAD3xaQLgugrC z$=eEEUO$~{iU;ZcUhE4yf^CRBv;T9s*e@5|*m)z*{^sm;2jv$WzS*ANBF9xnl6k(( zt`&A z2rhpr9z^DqBTzCzHAKmgMyJ1~^EPQ$v56QwHy3^*t`&+cIrsVJ_3RS-QTTKlx+GGG zJWR4b zcP;F?RD1w$je@sSbH2skkiG4W-K07|$}JxbPo?l4_kI%5;&!5U39|12di-7e_aPNW1wdHJ> zvcg}MUq@RB{|}fR)x5zN6%TD<%-;KvH{p>*@CEq2qK9|?Men}ByA`}UKkZ#$SL{^l znRz|Dd*6rk`v&h;@a}x{bPw<9yb$eKaB<_}%EeL0%#wn9j)KpRP(yBw zFQ<`piv6;MGm*!fdvZ4P#m6cj7P97^z8i|+#|qXDTWa9F`{98KU;GGj#dXoU4$7v` zNA`2uoyaaSo$w(x`$OV$oqaMx=XtDyhv4a$=epP4^N!Qo&O~t1#P}5_dzpBs>_o}7 z>;_YZXkCzPd#X!|ErpDfCGCC(N4Aa(-@y9dEu3ElznlCV0~@viZELNEdK979O-8pm z?`aQ=Km5z|_jq1*yZRPPE4Qy;tXKf=0! z==09zRC{n3vLSmgR;%&beqOW2Lcguxr4788v!ldsTobhs&X z+AA%;a*pj+rtsINpoL_J{0DGm^HndNH~!>|Bwyv|IodYQ%@7~?!m51|Ed>`3GH&Q| zVuO6z0i6S$Th1D1f3vRRe@p9o%8yh$&>#MT^d9XPpX@>A652Wr@8t2k;^;=Nz(=12 zhCE>2-2==mGk}G;vWfR&v}M`=b~``LUIWcgKEhPsV_YuZz0uQqQ{h{OS5Ba}1`tnG ztW)?GJw*SL=(HC0Hp%AD{q4l?PSjX&?QfEu@k6e+z-O)H@FQbvC$6a2;!!@zL)D%% zn;#XI)4bUa8R{SIK5FY1`Qk>$(4XOL`aE%@yLKbzyh(4EJl&hASxP?u0}UftfxeeD6)nW|p?v^a;(j$3A-Pi$M} zK5Uu!*x~c>YwK7OY&W_t=?|NAgzDbTK33aD{tLM+$IuxDR$%`A2g?o)=ry46Byx0R zzX6R|8Tj&9`0~9pul574)LTXtKMb$+;PqHJwd=8Gj?H4<9`=m*L~&i0rlRkO8fth# z+hc}CR#&NZD1?emz2KP<-4${yaH z>>Sm|*_RfdRDKRMJ9^B2y6<8uqng7UViUBQ@XCK&wVCe?Ga)c-;fMXV(yv% zKF8t5qkES3pnLXCXFeXsSSG-alfb3XopuhCa(|899Fo+V&!#^=6r2q~?=wefzl8kU zoW7jHzI0@l$@|Z@PFB$GR@TjPs2|==4*JS`YyYYSo?9$EVO*k}G>U zt5WUh3~tHg$IE@ObAR6@uVul4c@uLAURYnBZ-sWj`#R@c=exJyGpLWpk!5nV_8%fn zxRt(h=)=t!Qk|%5@`Ft6a`YDI=yAfem9NA#K!IV08YN8 zeTGnKzNK<4m0!3Tn8Nsg6|B!yWX=dz8BSp1h(;A(Wf_OJ@o2~rs+4W zA^$Uefo&TlZAxALKG#idBXf2;HcQ76=`*{=w&YiP!CbjK?2M`L?-)~z`Pk)SjYoOL z)D(UyhmSjk-q_8J&KOGIHMgFF@*`4pNp5&DaGM$(;LEkYq|>C&+&Z>J@`ad>U71i! zSo%!qw&IR5!zR<~B=i^LM4-R6*v^N$UA#1I)-G;oiBX4sL?Yuxc=aL-k{dve*@kPGQ>lu52VzS87WPAnLqmSZ;t;FBA zkX!Zh4!o%P)$%d2$%||Aj|_{~%XzPo_vB-wPdp@y4ee3Kn@3V`D zReNFUNe@aswtT&39XJ8lEa%Vod!_b%&W`cYfrAqH$uZ`tX6CFG=8D$BKvC13&;lNR z0lxk<>;9WqpDaFse+547`@A)N7yHX1@b)fr(MElbvQEy{cMzR;75k59OKr|EHS`1F ze5?2`G3J5lUe=D?#C)c6rqA=#J9p2|)t<1Ni-QOAc3X=ZXea&tbnYuQTA=ore(()04kl$SKYZEKEiaSrh2<@y$D-6HSHK6Cz19@lkdux#GE2Ylvi6t|u0 z`&1n`jLmND{W0}kM_=8$$JCR^i>BS{e-HT=%bt+V6Fhl7>jxd2VKUlRw_a`Qz1s$? zUf%&ei+6)7;OQ7+$HLOb<-*c2h8XM3@Hk_#?I>a^;81=|Gi{6CWJ`GQxcU<0nD|>x z?#t`#ng_;z8a(};tPPqw$~ben9)3KGtf~Jt|F{I}x45r;H z==0=F*uLn)cP3%?AiGCrur`Z0(f!Pv({qi zzAHHAv2GdX7!9dgUtq~jSN?`C+yMW+15E#pjP3d{_u<<;7CB9vw-e0Nc; zDehm;VD10Iu;9T%ms*P#@Z1i5YaYH}rEkA}@4D4myo&3e{+4h5fAYWV?F$0Qd!vBk z&|2%H{GSW{+cNKn{-3_XGS7=Y-W?KrVpm31=VbIbK1k@`%t0Hv@wMh)yBr(qE7H1e z6nr&-Bd?CJ`D9S|LiC{FFYsvaJq{W{vlM)fEh#zp9B}mVbNMRxxk?a zym=>M_6!}ggp_iEnB<6Y7DB=c&q zb8jTR=Bm5qcP+r5D$eNJxfMRSKO?iTj{D6U?_ICHBtOZ1Z2H@CirtUP6U#U^ASWaE z&`azI-bKgKBj z5U;qr@CrOqIO{a254|~$D>FRr>NB;Qh7sGWx+D}WqmOddEOa);J6DEwbJkWU6xUq07%Y>ipyk6~f$_f7E;V!@&>^{^lCWVJm%UQg}6^m+%X zD^bQAxb?Ht^Y^UJ!!M9+-g2j1M?`DX zZOFFrCA6;C0US%=txR_C1 zQ_np0YjXBQcj^>|-$xe1{6D?mrS*HQUSatGd*S0o&grYza!u&gg||*DCf?6_@64

{u;P+Gtsm|4g+{aAxsF`0Tk#2-X)AldB@_3K*aIJBhA+kDxt8|!(cX)+ zmqUBm3+K~bX6K9eiJHeWA4x|k4){mj7fz=EdvE$uJ8EBJP#vp6^34QaBklc(_SVo| z&B6);-zwVEI;7S}C-T0&$Mfy#h)rg#?(?CeU-IaR{_sJyRn$C$Z*;{0_C*?9(I47X z2e}@M$K85Bdu;uXd#Uupz=Zv*x{e*2;i+0H-nIdIs*bj5tT^`aQ=Qwe^B%MBx2&_` zH{e?-ug}c`WeyqK3_l*ihMoi8i+@VFrf=;L(3+g~0kqpZ4>6<2O#%#u%f^Iu6nhr-Z~MhB4%GZ&=UX+u_+o(*fxmLB{`Zl9X(2KqLtZtaT(USI&GM}e`0`L7v$6+^d4$D^;~$Ix-|l^*o< ztvT-N+jwFf`BoWyYS!=@TeR_&vp-p_`1~u}PxWv6niy{j@U=p#HsEiEwjG-pOBpoV zMqNf~t{i#HzIGJ7>V`Ap`sLjO1X^=O2$pIs`t)^&MDqM?Yft8G}2&3yaN9IUfSFHA+yl8+2OHm)G&aDTc565Z}j#g=7C}x z7tP3k@R5R7a4CEkU&Dig6Y?>j!S~U@${ARb+4Z!>(1G2lHK#R<;mW>!YECS%;-kqW z=ml)Ofh|UizL}VFi*jb+Pdmn&5iVhFJF+AgUo#N<7C3%O9{4WuI5UoQ&s06aBiP|} z@Zu}fTno`(nQvfc2R`XK`dPwy$dX5>jpiFvvjm(~X07d9gI?Qa^kP3VPhI574zlzC zwe+jZ{)dZ0=(pDk8a^M|G0BlF?!U+V2>mIi=WTw=7U(ICpuA7%zlYevnPMwxpQDrW zxyxfS+3^H>zcY0+(1)qArKxzu7a(r?AT3WT#ha;^NwW*aei=UIe+&0x2?|? zUXg0=ArIScMIMNKP=6rH@T}lUk8^qF-@U9WYp?2|K$Y3!RY#0eu^-uoCa1WXvBB@f z=y~#O%$^$IWGJ~)x9<5t(RKJS!bKbXv|qzK=^GV#b6Id*_Sw^`D*ag%9r&N}AJU&0 zdbZ5m*W8hPcIc`NoE@oWbJ@2+jtaKhsL(xkVHezuY*fOt)yVt|*+ssSqnO9cGx?)9 z$M#3&EUZX@)?f!=e{f4Yb{4LXzB}@v!I*utnSMR)a+%y-rkL}AFP_a zoL#H&-U_V+_i*5sKdN!NV=rgyG33sV?b!;vdx3YK6?>+QyPO@vr`OY-YLT_0 zhuYBTxB9Ie;)8kcfp_hTIACoHeQY96=n*?lXzi_~r90u3tLKw*p}eroPM*+4)~?k? zIc+q9`)YhE&Loe=(7DQ8Q2o7p(bU1&K5*8C?(;9WwM*w`wvdnJ?fY)lQS{Dk`gU^C z)+#6Me$Fp6Z335UhHD)-+-s_da7i{k3UNuTaczMvTj19hfMqxR=zcMJ^f&O3`cpo& z)(Qlh^l=Mwoz^?#TiCW#vi|DoDewn+*X^r?IY}^T9ay{^;UnAKtb2@geU~AjWgqt5 zSdH5kZewnUpfj4vSzjZTI;x>+eHnIRK6b3%*zNM2wQtWK9wnVvO26n^7Z$wVtZxnx-(So3KF-XT1HSPGLvz5l>J15|Cg>6A4X^d*+8}3)XEZ8qh&ylG)@O8m^-B&*}%g!g=&3FsI88xr) z50oPn2p7ZWwv7Mii7EWfL$A1VfabD8>`!#(vX!2>OH+V%{^~F_x>M&b`G{`K4z2fS zElF!WbLG$QQLU)-y3)&`gJehX93w}_P858IhxLC<>#68UANoOg&dPE2#xghB;F;~cj zZoSiDpA0pLIr~rfK<$%xLFE0uWG_UA6?TI33g`8;nozyj{T=}QMEjlCy6pR`LJk#YkPcPcCT8}sOJ}hc5uJ5f{41QJGek$}CAY?Mi{Wu} zF|@boVV_N-oN%*m6koipn)l!(y%&W~^lXg2O{~2#7@rESY_@DWtQkCL|Cz?F-}A6- z^#1_<*B(CcRD^j?F+O*UHXeN8fg0;n+MhxD@QLCpMsFa$v~O%WQgGo^E+&T7w+se@vVImN!U#)ZyKOqKOSU0-MalK3;i-bh#W z6a4AevEM+4c;$aIb#&A1SFIoax*tBac$MFW(5oAncbYk$t_+=9fvijSC=W@xLHa_m zt4%%V-rbYIpJ+RaF+(S<^ENrUS9;gPwZ?k!9tc0x(>r6qn|PRKo5w&ec&kmmi?N3l zUlZ;{hrICD!J%Y8b*ZCcg7K^AM|w&3yz;GQyyL+@PMn{b&Ib>AU=sZ`2HB(resq5F z`9OGO%Co>c&Yr8h^|CZqNH;biA8yT#@yLX9Q4?|@U8pgo!>ntvSAGHw*q^aK^R9XJ zxsTvSG0+#eE}v1pqIax&sJEZaOSkpJOL^kyVd0z7c_=R!{{g(?&L7GrvFDCt{WvqG zk;oDB*#$qaK0%Fm=)^d6PiyZHWaH-t(Njb8oUsk|OYg=sX2}=0lFhPQvJE}u>;8EQ z^Eb~{GVTt|Gb4l4RaRSW95%I1+ftn^Fq9fy#GsXfY2za^{5AS*9!ej)ztOI*vtO}N z?fr@k0q^v0_o;OUt>HU-#r5UnwN;|yrV$sB&!#w?jbFEKJ?jsDgTC|ND|aq-##CI*d>!MAcWqVP{3oBZBtexoK6xMK45V@d3^laGE++_d`$M zi~(chL4E_H@~vcJzL|x6NgGk*Msrt_>WLT9?ks9Fs_ylj!8mi+Q|y=7e|Ql-Qs3N~ z7Wz)+xa4;3W?Zsuw6gUU5rUE82zaCk=;(l zAK@e4p%d8|egSwGm&8MqxxS*O_r$-*&*57bC(n0|fhTIw3nBL4Ba51^r9)fGZJw8Y zbobPhp)=*paHZ%i+dZrK=QY{SJFV zzQcN*^r7;sb%u`On-Op>x(N?a(GENq`iaM&U6Y4yJD{84S>Dm}T3b*qm8%mkrJrmk z&rj1<-`~Q!H_-=h*fMOd2mXj$E9uvz=3D1HP(PgC^UVML@DKQ2%*W2( z>J|PrzYQ$s*-I>$Kg(Xz^UcK>9({V!$xpd{M5_F!%d?wb5?un`7V$xz`yjhfc$xgH z{jo~fH$LjlWpqAJ0Dh5m$%WR*3OMsta^lLisnL&YXCU+T9$;Vissrc&ct`D-eWA!~ zGi|m&L)*Fvd8(lR|o54>tdZT3q zXA)S{>ZQ+aZ12_Zez|4s5N*V(iYt|JzZLy>i1oQrYNqYtdn>h*_E?#X?d1CFjH{f! znX9$m8sEF7oAH&ByD2>v0~hV+=XT(9^>I6JQnRlyax>q^OAL596D%GD-UxJvj6~m2 zt3>sJ8h}N#6-`@dzZ0D?2Dqe8Bfw59twy?Adb^r>dD4xe_>M|W(HpLQjY@{Vk@Tz4 zsr0Kk0A4rxHKxAd1@--N`c|K+(~g{W-iAD?f90sv@qY*Qe9IUco_**DYGl>C04}9d zT{v5SQ@U3$Vjt9~_L}Hk55K$s%@tS3W6yXLd~45xYINZX&SdV{(Xl8P|2nc}U#IRS z{p;G8yYjpKk?WG-b9CA)_MCHmuU9|FcbTJnGx%0|;@kYz_qX_7!soWcz3lL!#BYE2 zc7Dq?&^oE!{|X=F6YBlnI6SHtUK?u`;>Bv<);PpJtXIyQ@AK7Ij8}W&A0QsCK8-)Z zysbU9=g21VVape?g8WTmjJD618@`G@UHGpszjam&@nrKmBRrb#9(5XYlsqr_K;sJ|2OL72XL-HV-&rh zeM`|&p5c6g{p43DFEz+sFyp(z7m}&Rih&FMkHVw7@$Ym_uKccdHLjb3=DP6_E8u-- ze~$m9c^rPEhI{x3Z7P>a?|+VYQF|ylX+v|pWEfh6@&f%j!|>7EyJmN-U`|tQ*mNFR z{m=9bU+P`8@z(ixjed2Wwasg`-Zz(IqC6k=ec7% z_nPyZWUl=C_$9fKByNE4%U4{#{Skq1#6c+CK>oNFXGwWZqtb{UMu!@%EAA>5pNr)L8bm zjAL)p1olf!Vh={W59J8l| z{RqxEYxe(h92xD4e0rZt>GN%5wGJBH&)o1D_C}07+w|2A?K+^xVc<1&z@XbvXmt#F z9*34Epe6Z}@wcI!-b-l{y=$*q_9k}FADue@o1_mm$v`s>Lw9VIqvUgm=Er#dQ0COm zqu3X(W=^GcZokgLuaj5!nibz}^=mwV-a6?Y;G8$rZ}k%BwAdP0b24VdALjl$v#j_J zp~<^%Tk(H}U*EgmiZ5Xe^=ef}Z+2I% z`)2cR%G+&p3Kl; zzGaghd5|0@)r(-hNXllP@Wt1l2jDC9r#{sFkq4#o$!)p`egUT6;QJkYgEpsH@x#2o z+{E+yhUbV*g4uar&mWn}yKe-UA3b>*(kb(8onoKI)`#);3O@i3M>?6O7NIwo`^k68Mxb#foiqSZ{t7o@7z4nn)xLB`Y@j*%aPk)&(^WbL~C=zDQf`jm&F&x9#H@Q|6YLw*JXz z)H%E^4vg-hj};c@W@pouGyZhlvrg?62ICfXV9en|Tb{@vcWhVfcUnNLLFJIU^0WY( zUh{A=x0^N1-nPy7ax~S3IYy2R^U8MRU~f`w3cmm4^YA@!k>lKJ?cx4UxUYO9<%j+c z_g%anJ=~pw_nV>3U%2l+d(iy0W#e^zzs5&-ZSoucZ0-%F%|YRp64wLaSIu?%oYc>~ zK<*#ob@j(3_-kfwJv_l)#>S|42_9*JKVt90E4$#8JvOhbVcc!M<2ui@%|cdV(5@3& zE#i6R6V-3#T=}r%uGh=m$X%}1U=nlKR`TIuzk^R1x9(N_&5=8JN^_~+m7wx_vI`2=m+C(_X-DWA~jWW_sl5ER&1(JN6i!6K8Pi zFFt);{nRr_e>c(_o*V=p<}u!EWZtrV-#}hf?|Xw?WBFA0`ae74UW@$*#E-0;@x8Cw zy>)EK?yb7M#y8;E8CNae%AWoG8=1>`U-jJ9zD1SwV@safdex1U_3kr|`vyMys&5d_ zuGo5GQE|OBz@pS0c%4B0PR{MJ1v+nf<@HS%b z?yak=yl3nAEEr+qY&JL>G5EQymtI_1@7{Zv`<>>Q6=;Hn8+H5J;p;ykg3 ztapC=khlNo$i!i_XJE4X*0t$9TnpnrdY=tlXYPT&Q{azh((ngu&x1eHe;WQw z-)Z+ch?{0-gF;(19;~pevkHL+s`4( zxubT~&w&=xFaMzahO0~JpT6Yz_3vlogdZ>1v;JrN{!2#R@EU&qkl#Bp`miT{&-$7C zemJ9dcpAS4&-{;i_9WI9fR|(NKo0%3IJ{eiJS*ReSjzY*z!>13U|x=lOc~r=9~iu= zz5o~)!&6fRe^7tj#U)((kNT{O?e@e|;yqVqM3B!Y^4a9zNaszsc-hT7Mlu4}X6BJG`5~5!e5mFSx-G`$YyL$GT_ZX!lm#(>@a2f0y6w{N9m~6&^fu`BuSb zE5F|dXM<<1*!t;j6(@1T zn!Lv3#ZM{l`5l;S{OsNu7;N`#&O|tmbM=w zuXHQtc(z%AZ2@22`jL(vn+=a#d*SY_VeW6^{&Zhp+n@OT3co9SeV%=kXD_?l)?rof z$+Z_gw{>cMWqnW2ukrPM_89LT=iOI*eV-k0kAK5J_Dl@O=>P194BEMJ#nvl=w!SzD zZJV)cUD|3wI0t^{oA1DXDQj=@mt}5Pu;}cVEt&u|2_6LPWSbDcG0rz z4P&k>F>~AZm*s5G9C*jF7dJf2SiTD^qkVn1Ee4+H%y~cL%L2?8<1Bti2Q|JrTWen`x z#GV)ACBz2OcoX-$@Z}o#KAXtRvkEx-z3S3X`!lC%H)%hCH7N9NtaZ ze9A`ppPzl8xb526rAvw$&^7XBq9f50WAJ@{+UsGHhvK<*;3pr{M!4_p^R?F#Y*^g& z1i|!m;mWCx_cvg3;kn+d)qEy_kH`djT=U+X8s7pwwtx?9`Wl)|U z@{EtDnoi1v(K>A)`~o@S+IOe6uxr;^Rm0em=L=6N0tUvbcFJkr#iiEVnxY%nz06ZCp3J%$&$p4283h+j8b51(5w4};;W|STQ%3<9^R42)OwO%3 z;N`VVL#SuD@{6H{Kb@}a9Bb@w;JrAsT)dE97#hYk(Z%2rpIf-hbaC0Ay)J3|RnHvp zLI0r%e0F#H>#d7}2j8hbRlD5m_bmu1UnT;*gG0d0?2#dCv?rQk)U6}dpnSIaKc98@ z-af;JXN%Nt8+$+DhoTk_{T`qO><9gaCHgb*0NQS%E$v0o{%7m*^IPMC1}^)Y z#B%nGJ`YSQ^DhmpTu?r-j{dKQ4>-5w#7g>G(Z_$@wuWBAKJ~k&#PvP5a!NdzyJf>; zuZP;X|6Vxd-?S)ai*dxvx1D<(;dg=$9`w~K$4fL>A)0vVPOeYmhnF}%%&QL-|3IJY zke(B-+I`#lt!XUsgna0kV1e>&J|BASY4-dN(RFe;z)iuKDeIcx;gV&(4FzBFg@(O- z*1**;TzSspLx13T@V~uaW8aXEx!KiWb65jv9um~K*Mr|BKSjSspwE;a6(A4Y)iJTL z!T3e;XNR)40^eKDRiU?)1FJKvmZQH#`&ICP7xo}_kN?6mwV&dXz>^~{MJ|qzmuJ)P z^T5Ta_|f^&tM2{k0+AEuRAvp-tI>@}3 z9Fj9%KbX17Dzbln_zQq0+Vj%b4wEV!mLF=x2>0f;GsOdi^Jee8?nm5!x^M>iajyBY{`uaJ1 zV&`0Gze`q%oV2q~dNeh#+tG1(#8!U8zQR%HuwI{?a&GoLGdnpE&i75&J!Nf#uVf|!N1MG zoD(kLf1Rh0kX!VdblZ&UlDe&@`uWD*Gw1sy^MNJ1^D?Xn%9jpUzM=s9F$G^xo|V6QD{y7TGrHdle`@WnC!N^iHO47<)O?l+&nUlF|EnI;W#Dqx#rEFvHsnP1 zl6Y|oveAl6JZ9auzI&o|Qn6;?Gt-d?A2KMNuiWF9BNw}gE7&nKxAuUor|Xb`74+jn z23iLF_=!svjF>37-X+;!uDlL>Udo(H4UJG4*A!p&(ykPq65sdK_8jg>Hg13(LGd1R zDlv5896$cQhTlehfw9!3S+;GL9AfXH?BQQPbJxC0w>SNac?6$2^3Mu@%Z<$kz{7Ap z?w;-E*Pg1KP2CsaUpWKv<7YG881~j2zK3U39Vol~lw+qP?M?B`9|Z?<;vq-Z%LYz` zCutwaKlncKKU@BR6)X}?WHW7j?Bm-@|8yYLUi#yvR6pSt4qgP0@e|5zKVgo?Pl$M6 z^}>6-v9FT+y-_tE z9n9zOyJSxDGxPij%NiPbk?T=p#>ajzAA9Y*^L7;e*7{d}oq53T0_N>}XfFGC?xD%? z;k|i>xljMkK9rGY^GVut+u03#r>QTWfG_5OFP41g*Su507z+lE3r!AG9Vi9PZ@g@e z)AfZqyzR7k+G)#(e~~%`^tpESqP{!*{3ZsulRa=nx_0-t@uAs%Yo~tCDIH(r)wAN8 zvC!!ehYsQw>gSkzrUwdTBMeU1xc)Ph2LoL{I#_81--@yKw{EF*GM~LFb<6mcTzB1b zu&$3iHwI2DIw(6>wwC6`K-c1fnGL~*vI5ly?zgfUGnvz)zl-h@18J!2K?tMW-p zh*9CUG|oMK;`)!@h9>-H6$F7|~gE3Nv|mCpAr`#bA{6$e(* zhh#`~C|ddC0Yd|ASuf^$ADS!e zTAKJ#-6ciII`cWRotgn3+o%o#PuaOO}XS6n0h@Rm5 z=*Fg7=!<$T8Q}5}t>ev7pV?;a*)ip8t=22S@%HjyCTpLS_%7OyrWz^GZdNe9^d&bI zVB>whzv~a)zI68L$LVXm(^uie(9^oYscoLx*FUB8wTCgycE%)KK7ui=fH#Cg4*;hN^x^yjdlFLUdS+)6FJy=Te~{)xTkKLN%Y z0@Q}s%~>Vz%1^0Va>c^WO)NnE@b7oD;-d)uP52hOwsPi-39P!U#^ZDNE-Vu4Xd&k~)yT2efT1~#W&G%nJk9g%kIlm?+2ONAC+#B1H-?y3H zs>50NL?yrT9A97hs5kq8HIMy`wYnlG+o@ae{}JP{<=*i{Sr=1($JuwN{uED4oiFnex(}YateUwpaAx+wdvo@ze{`*X zL)Nn3L(f+QYVPCz=lOqS)$Voo^ShbfqqrAX9;jKx{~00whK8!luy47$SFyzCu(MN7 zC&tabi=BeyPYx_iLxVfL@gmjjNQrOYn{}64JM;hFnc6|}8?ir#XY7pZ@prMelJ8Q! z`CmR9&!zuw|Fh-z2i2CzmlPkle=hy&P5j`ZB>jug%hkc1u~L5* z^+R@cR|e|46*G{YCfEu<0qQ1v)q{0JFmGm+SD$|OoVnM6RUOh0df@O|NHqZ8i@}s zhYxna2QEH_%6_1Z${aub4LqhC9OkK+x<02SxRZ5)om{Khrf&; zPV7F)GvY5F{FRI+=Y<#Zzr9}I#&A-3D9$+sE)TuOZ5APF+}S)KNKO*xB;|<4 z-gut@7q9sV;u0{?xDP6K`i|C_+Wf9Ivn zSz8}==PZY(9Qj@E!F{a<_g`YX`#kdN;=cpi=7au8{C8KtV^8{u8ipy}*(Y@06Zl8y zGklq~bL=&OwR2f>*o^Do-87VaA$;ey>yeV zEP<8<__0#Y;BNd6*K$n=p;{NF)r2ebjNdj+ITja zIldq?^E~5-5tBIIIG$!4yRkDA!^vkHe=qDm^uT^>R1)?#*gtx}p^5UYe8l#aV{?_E z=gy}$UH$8hccED?O7iP-Z~yCorawRP*4&Q0ZwTFAU~?>AN-xVGZSw=&;(;jPTt_P4eyE8Vc=%(vEU zIaA$uot0UW&GS=ve#@Hd4UbxZntj-iyMVW8`+e)Hx8J)y5c$=C5zO&bQrLOuTcLX0QUsk!U{7l)w0CvL&?1o2qzZyF}#`DTW zlfBW@pEw%v3GGFVv1TJZn9sV9WsNnuYUF15>{i&lc4%F?jM#A*?e~>kq}qoeOVGX08?AN=71u@G)_jliqvG2bzb2#KEm!jtiCG z!#AN@W7xOiyHCBEHV->FS(?g?Q}CH zt?NZ6aSo6dc4u8If4JiMp~KWX9?t)hwddFwcfMjUFJ^`|5%ag#&Frzg`HDL> zTmNcIpJ7bWw_bZvF_by-`@vmPDe(zx$|h_|cYZZC26o#yHU@T_Yh!30l)dHk1-!PL zD=#mZ@h9y!m%gzX&{sN0eC5!%5?rhxPBgiycVeu{-O?Q21g*6n++w}b`qo(Npj5j4 zGBI{%ZuHRg{{@qD{i{RQD&)Qxf9ia4@529pvGzAo9xu*mZGND`oJ0{24By0C2*qiR!eVc~|U3hX0JjwM=!7~7OEa1_e zX2GJo%3q#wYvoPulD+v%TK^9_{U3Jw-)#54+tYvN|Dyk|U>DlsxBLHjTK{+ao&DSV zlsf*`i4Qc`@l)kjB>36*Ij+qNKPz85ip?y4MEB-mKf3&!!pEi-jScs0X>ecX!2RE8 zc=LH;sVVl2p;t2BLiE}Wy*4`hL#=4w^>wj6fNxaN6&)GeDP2Qu`_8Aef6TyQ$Csz3 z!BXhJB7f2FP|T)bfrp0TFX=Crf9&c0<#2i<4%F>9k>co0-0eCk0c-|?_va-gT^<(s;@_6s%dzWQ(&c+r|+$0NIT4yJ%;Pc)p zc*u!Ic;|k_Qe@9xD!B1cWPb=x?w`xw^~T;l7rrvqyGwwtkUj!@wEn%Dxx`)ncHs>J zFSg`(v(62iF5Gi@XEXOc47P6eO`7!rU~}QgJr6v>*CzTGFOJ~juE+Z?4DKAkhyN>} z?M&fNu&w-vz9UZG>G`golwP-eLVB(K2zov1z?Dv~p;lFqyLKWPvL?FLx?FXbfBb~5 zyZB4DKMKKxaw!x)D6#Rk6Z{!`?H0bsO(@`__0|a=fwOfEY~FlI@`}(ONqxU@IQonH z;K<{49^R&G*6oA7otv183MPI!lt&!8fqt9V(@?6sP;|e0&(Ap`$E({7au0pY~1+-&8eKwN^csypqYp4A&1H68@}h# z#{K6TFYolE8xMWS)<3&F`sV>dkEFjC!Ix3Zre}zA=Vw^s@de|<^XV79o5#2EgC$1= z(}Fwa^7$zLQ1IKZ*!H%or~lo7<$oL=arxtX@L<0r;Th$?!*>FnuLBRCzZagLr@_NI z)d{Ql3Uk(yS&x$bnbr&av&`t9bp4e6zdN4mjlFR$eqU$Z7ug)&1b&0)0oGCD)HVuz z(>ux-MsJt#^Fo_g6As{~hGzb8ew|KHuqYMB@3^t=B(o*8URx{rlj=TxI&-dQFJ^T=x6Q zlh=5!bo#yA1NW4|1l*?I6G=GA(C22H6FtvQOOA`@Pgu8K8H)C|`{DoX)~bs_CC)g= zH8i>UWkIK%DGB)3TCacEJKhu4&o2O88{ekg*~$OQZeV_aPs+~tYz@^6RX z-_&^I3F1i+JDybZQEjHqA9if=_qD0M$4_*g2r-fvy35$<<#t>t>WOikZ-0;K%z0zY zY4M4oFCzb5efdyY-`{ik{y6aoH$QGP^%A}Lagq5}yqH|C3g)x!o8;g0s@J@&PfKY= zsD;>b>n-GT-OLC3qMaPCLsoX@TyWd8i20!c-;FlxvuBhmZlB3Fk2SiL?B%)Va%$P# z!hPCS9T~N0p9RCZ+fz8STs_xINP z2%QyAw{c_Vc&S};e)k0O^zpsgrvfLsx3Vs4!>2v%+2KCyRcO1VYG<3}Yuww* z8sEnK7JN|RcJbI$D}FiWvlr&tbwp$2wrwU}r1mS>%TNWstpElWH_AoRS$_L0UyX26 z25y?5tEo3z%D9Sc+{B2pxj0cy?ga4CS`3`52?-yOQgAUAdyePZXIa!7#^>~9)g;%p z{oy}gpT#D@Yv4<3?=kQio5eS{jWM1|#!HQF$jz`h08TY+6UVjF?|A3U(@y5MgC z{!P#}V&M064hMeaVz}`3@z7BH=sogZ;^cqqzn|POooTia{?~Q=|F8V+iAE*Nf1;;8 zt-y94upI)YlI23d1+Vo4PZRgO@U)Lf@|55h4jh}|9sh#i6E%h=aH{;lDB~)|Mj6F; zyu8vAY(Zf8cJDK_-{pfXiyeP)XpYWA+c+Y$wcy-qn|rwSE3Rp5vTL;dCENGjFWPpN z)(3~B**Bw&{HM;Z|4IE&8`jb;qtiTX{lL@Kai=YdKUd#fZ~T#Dye3wiQ51on$xATj zl&H>?>LnT&@eSV=K_dd#0R?H`l3L*^9sF>6)Gk7|ah_Wv23t$`fR0T#tk zux~mI9Pqw@Lo(pu&v~?4NdI{Y3MbmQv*ScA9ha;2N^o3gd^&w}ww<##Yp4geF`aE! zJGtI_UW4LA*HeSlo6n{=Bl*`)nflV`4*ic0wqLoP<Yv~Tw15lbxbadxk0>{j_E(;{ zcXbOm+-L=Ac5^M(9W44OIn=&PYkW5A+3mzm*cV?DrM+g_s{@C<(G4X!E8Nka(k(GV z2Xt6MpI&SDu8BUUS=o*E^U2J$_P4X2Yt)%lt0O$`_gOW~^w|mxY<%1Ae4KXEpAl>t zUwZ!^_c?cers_7U4exuYum!@OB2UclzDK71Rr^e}j@=)9lqR3{dV5knW2L?#*}z&` zmVZWW$;0;%|Irx(ZQw%ww${zH&nBU_OG})*5=WjBxYv0DoMBJAv;|tVj)Yzb8l=fS zd?H=)0}rg?QE&Y3WT{zjT%WSz; z&0-^~H*+?s=HdmHJ~IG4AbHi=bQ}C`k#}LA*^w3QC%I)FE1wO|LDQA+y>zf-SapWQ zuXU{bs+N`N{l}OeZzUf1n3Ywtz_RDg;jG8C(0=O}=0S9W=1SFvc41oxY&rv=bqsSS zdO&lh;ze7~6&hED^o|)%a{l_3-r+gHvf0rSg~-S*{O%aflat*zg-S&!|0{mz=ZI$i<&eZjZhHB{RH?-wD3*?Dx!a=|vuw^vTaW{B^S0p%?X` zKj{DI#2O>Hpqxd5&YsWMq1&z4zK8IAmZRIDL1?5yL-p5MX5sr~cTP5A7M=_Z*UIk7 zW6aI-!Fh#^Bjv?oV};tvf;+Pp6g*RgJz#Wn(q1#~4fMRHoO6zX&vp?Htz zTFf5AmyOLu?kcvFbgSyPD_%GaJmnyYcAtj?t+ES)LUH>sOvp*4DFWrdUvjd*OK#s z_5@2`>-M2}r$ixmQB|d;cA}CU z{&)N=BV)qB-^gjyyRs8>E_Fn*h8#)O4ohe8ZaKV{n6K&cThi@%PP!){bGGisrd*rw z4XVb4HVyOO*3td09Ny>R*wO!zK@!>$>pi1QK5ugUKo_;^IQD30OK@^$P9zd-( z+BM<>;WPNJ_hNV0`K5n;P=gKQ(*9IE*V!!VV{P>~C=q{W?v*FEqAbGXGZoiDWdX z-?2a81?`{H`LU9v>(S-Z^JtuoUY(9!?Pc|<8N)Sn@=jx~I?t;nfXYA_( zUVZ?*S;tw2Gx%-Jm;lBW=<+c9N~~Z!d0KJV&$gaO@^d@)e{Jk&@p2|Iui7RVMn8*} z#jhr>V35nx>Hdbp(}E3p8(hQxs*zyR;D7(TyFW~A+Op=JE#L3rUdb|;n6mDw2k*O zeAzXHT=QW=Tbz$7-m+~V&Bwx{9WV2?;p%#QyYiV!{af#PrgCh`u|F$E5i4d6tRyy{ z1N`iJUHl^YSMjDGHT?CzMV#%Yk}2Zu3yIU~*%EA9@tWpscb!t>B=>UXT&ruma?>P- zv8U_SPv$JAD!otq-Ly^oJ%Yb2IgT-&yL69O`*PD(&|JGi*G3S3rf;2}R7oto9UX&Q zFP_F)LM5^Ejo|WQ!@yc_8SQxWu;Eqc=k?kD^iQXZ&tAoTGL1{)_$u$QH!7rje8rqf z<-4P|3-H~%G5W%Fqpq0ew z?fc8jHSGnhbYk|EtPj}tgty=me)F!4LwAm|W!9aGGQuOs$ue|sWz6x1fBS``4I?}E zAJX*SOJ=<->A%Zocm4NLV(TTWx%uz|m1m;(NGD@!f-bVzH#5InOMG}GA9PaV$TRn^ zmJae;*)_?yhwZ29{lpxAjA~uBg?5rL4g0ynWmtWH1wN72cOGieOQ4uW5|JxlH=&nP}o zZUq_@qhU`)qkMYe9F47v`2}K7vLBu!22gIUpC=x~Go~%tNY0D4-jq$F{H@LDxM6M^ z@4<_Af1j(bMjLw~iH~#VH&_2zrdE#Tx9gb8I2Y2T(l#4AVezhXX;F?$aC@b^EeeaVl>5qDy)F>H`#*(KmTU;88e zJ0rB5^=kE{XZ2_NAE%x~{bw{MChJMK>t!|n$LnQN*sr6w&F?Qsu5EnedRhOEUN6(y z=x3poi+5YM+G}R=fwaz~^``W-rlIKE^R6{v=PJHu;&7}7X)R3cYu!y}+;T=)SZi-L zBA;4&L*Gmgujr#1|EpPRDdX&l*~rkHz~-*|9KM;n2kKJDkCP5=f`8hlTCC>*Cpu2^ zg^Q>89z3c4R^Vs@CawDk4*92r=%kgbM+I0HNLl|oMZI!8k8ZyYyEJ|MPxh){?P=}r zI>AeSF3jm`e{-q19Vtbo(aZ7`)URwg7jE(PL}2e}?eCY!Tu*dzahJYkHq3*&^ff$p z&CKn0s0Uxdjaf5;f6ljNwhG#Kc_j$Veik@WyM<2>UUht6YR8QTMLu)xwKsdX_7|>c zEZ#W8AIBwi&hWJPyQlRr_Me`2eJt#0D}8|^}MUclKSms#_jcU zWbbR z)0Oqz{!CLBfb}2x!Oty51||i~8GE)&viBAEnr}gFH(;BTRqd3FzJia~USrFbwhe7_$E2;Ov^V_PY-@5PQwYMW^+(;-`zPwe!huocezE)qBa8 zDDYeH?95<%WVRLG0l&#Ea_11!H)HVPvk6=^p&u1@YAWMfxGe`4^TA04^T;A#cImVy zBlP&g(1|wGZVH_km$zN7K1%G-@zBvLBk$yv8J>*K8ZrtRZsU2-FHomrQJo?4e=4RdA-bnS6X625cSBsUYs z{T#fskNOyHJw{un*f9_BsP=g@iBAgorcJG_>HO}NvG5}Nn7}RjwZ10Y8X1tkkP%*q z9IQkRYW5bVy?&OkfInq0%8hLHVV>|LFd>35I zI=eL_^rffW`b#d(y!kqx^jaYNC~~`jPdd)8@Zj8Que$i|F#caMA40hgG4b0_cpZIa z>rchXwO*^WfIxUL`_T59dM;j=?R#a6M` zyGygWx)+fnwaynRwye$wb6`_7cp~=l3wRyh+Qd*5tI<2L((AgK7ZDGnoo3nz^Ss-( z_#lhtVsEn_a>Vnb@A}uIBr-PqJ5iH~Q6;*O?g}TUGjy z^V+2AOy1#$YeI9#(=>Y2xn4(1Xg9vcT#NaKzO~o75WS<=l}feXCcZ2qZg-pTMnA4^ zG@N)GwywG6?LUZ2b$4G-uXxANerIY+u%`;D3PaJ~frlqqQ`esLUw)C?P3laPXPQ`# z{JkswgS~4Wob&|KM&P%cd{ghUb&A=+19jjfng(;!KmS*m;70IjEc%pK^M57Sb@8POK|x3a+}Yh=Ky0BuVzFY~O(r25gT)kE z(gN;=Bw&cj4q#)wG&d61t<^P!pQLIx;gS$ze+|-FFSVK3TN0uYl-6A!?En2amzguO zv$iqj?ip2iOE39wo;j97nM+Q@?LnLSw`KT!Fm>4L!J-cc1tV@+XgjuRrD4r&wo6 zBUjn$(AnBBXJpo0uTit;43=spcWq&O)`mVOHL#xf*&K~{n|k13cpf=9mHKd7s4@M} z#L~3`e`EF@9*I8mrql|2kG}p==Fip~_=gi1KSfCl>@C_4dmPwR z4nCbsJQ77yQ(yTQHR<7*%zZq63v%_=#K>gyM|v&qEM*PI%|jYHEQ1_s&da+A^Ay+wHCZ?Wz0>`|A!c> zX#c#hzpq=~MNV}_UwDJbmkw#a;RO2od)nUuEe_nyT;LD%J&+rJJnYMl`vGur8+f~& z^;7OIVE<85M@6d7jU7JR7g6_H^|HdTpK}7RG`O*z>&DuDYJhh)R%FeQsPlA_>mH7^ z^aR>pPb~Q=)(O@tmh9zGk+lM3()Q!Y&>8nrhZLHq?I15dHxK)wPgs2s!uHc|pMVUS z6D{lTF#U#d{WkDrVamyMXoDR0X{iIC{;PdtAIcEWmM_&boV>xtp-KsOYV3x+1HdRJ;A z`wT+)Wv<^b_WB<=I=k=rQDp9!T~`;Ov$XWd?^nZa4gTfckbr)C6yDjb3~L`}kmeNveEF{j8gKZwvM92C03wpL?or zSC5`FG7ed1#?P~=y|DjLWY#^%ti?H=^)hQRV`_#zP3=2BPxrvPixVTKhWFfDB@~zJ znULC9pg-PGI9~ddEPYD(KG+B1^PPMzTGQ`Wv22}*U5q)bGf@JqA6Q9DYn+;Esw-~B z$}<}4LyT2(_3smdafLErvYqFU#z((Zz;_Pz&l}jW!TEhZ-(`EG3vA|$(+TISufjj!VE{a@gl;=^7d-^TiQ&BL-aJzYx&p3wCIcqk8q0;kfYnoQUz@9p`=lydOLUyj{r3Fdtq%wXct9_zt^tCw5ba z=cB;gk8L_&Y9``8Qaf{t{6cD64p8H={{n1k?9;;EIUcp^hQ6P%ylU@{dp#+?oAX%C zwtn}y8edX=cN5nVBYW_>k1J2G3u}w;=Z}e({5IstOK;ET%kkvNxbjc-YY}+A_FIDY znG=L}NfG`mhd=k@*UT^A&%MyiBk*T#y|YkOdwNzr)A4A&$TJ0Wzph{mVf_%rNez8t zC(4&@adxX@gZ5`9Sr;Jx*u_{sb$>lEye6)3mcn+ey>??e`IxWhR5dky8@le}2gEV9 z7e1SfVSo#N{(^WTr?<40Z1+K09?1RnI@8m47ra?0AI-S1_hJS3|0nQ2K5g4R#>&Ma z*9-7`MFF0F6!727dcl{A-ziT}F}zXg0gbkoca0)%ltbFc`t1d8cx|250s7}AtX&2U zEUrp9n%L^~6!r?tCm%;J^iH&At9cj(dO3UR(!_S(uIey+ z8hrN>uOfyrub0>n@vwQizLD64+E~FJwxg%g4&Qg4+VTCD`F^!*FL&=h-MtlW==lDx z_}&!Ud%@jnoY?XGpYi=N?y2TOoBBcyo4)wnD0%GK>+|o>-hRHnBWUj@?%qz?`yt<_ ztG!PHJG9&TiP*9ju={rQxo0zs`5*ayj&BElr*`6*)UE&2w%fqZPp;sMy!+;i^z&RR zwzu$Fbw)H#*B7jvGt$VssLgNFhJUt?XA{`qdUg%@WQEUugJ+dXHD57rH~-+gQ*)`j zgAV3U`7MpayZbnwL1zl+T!!6ybbiG8?^j19OILuChSKOogBxPH8QQBRcCP$|XYf($ zcvovCs^5=}qOK)1{DHGcb1qzJoHg&lHg4Z`1=o%R&qshKUbc#U<4ZeezutYX;k(|O zLfeJoQ0+i{VO4iZwX<1v^KSgc<1DBE-vcW z-s)ruMp~ zStApkmA}@1K4W6tOt@afpZ4OEdj5i!@A-ybQ?T*#^~w!$w)~g*zS8`*wNk#o_jUYz zioZMg^LdZ5`&0bZ=WTo%e}J4a#oy0nkE_<$GSu()^6?O{pRgXOodx~gWhB9q(kul&h}u; zI{)yf+E19hr{_^GK<9wmxozYe!E3*RucQ5(B~u!8+&@SSnN8K4g<)`D*MZ*)j?(8N zBav}W;5&Y-pZxo1_?x2VH2AGlKeUzMyYk#t(ton296o8^wzkLP_kF`gmsCVYg_nO} zZ)Lh7ar9fvvDY8z3gj;R>N#EW`07H|6z(T!Aopnv^INif^W$1OJjB>z%OB!<$P5y}jXSk}_7T6g-`X)>x+lM` zz-<=qBZnBDsVl(xr#Y`Qs8^|aq#^#vozK(f>i;xX@6`7l5##^%uk-u9qoD5(xxTaY zPF>%}t9N>Jk^Zlve{|2I)M1+KVNhKh&Gq~74?~;b!v%0$;@~(~fP)nQ4ul^+|0je0 zd$4E1zAr22`>fw0{y$QH|8olZ9%c_7JU4Q(_&)@ny*8TPQ7MSEf2|6b7d-@CrE^(3rK>Bi|# zvhE3BQq8f|%Qv#mF@CrYdADNuCgfdN5qW3#K>V}vmW&*acR$PbFL+pezB6+8R>A9X zK>p3cyxGcx@#^XRrto$EZ^!c5$5$_}E0B*C-bVs>xA6VuBJj@r4dAu&nUkN9sTVmh z^%?3BW{7j9>(Q4TK6k^)P3fq&(N|%~;d9(_ zIq>ouEj_zyq7C$t#{V<+l6svO#*-|(`T5Zq;N>isk$!w9<&CS>s%S3(T&!(9Ci^mt zy=?NLF2g2kNQ{u{{9p$#+x}hNHa4j0lcli@HKy=h{@7eR#=-0Lcy9o+^myoBdwx41 z_=VU10r>Ab0r=M#eK;2X!h0EFWlLgKN_oh_x4}Dhspi?_zCB646L`hmrXY@%##Y%P;C@jlMq_ja)!G zg8YwI_s7f4I$=+3$JA5?x=1Ug(a^E|SNEE=J+8B+3BM|*xK-;y*G)C& zN%n2d)yG}c&Dx5|QL}sBwT7>@|G>80Y1*Dk8^lg%qbHhjN~)3ClzpsSX)QsyCASu| zp)=&u_+ye?{Yje_vhxkrj#CbyYLILJ7h9-tq8bqF@6*1??yaXp7Z1D;%@z(bt6YKe922$M{Iq0)X6B12ao5n?lVGMI6TjBs$CtEQoCQ>|4iFyiS385rS1{mFyF~jv1Pd*b-XjRlrz?! zsOS6(^fxh_!;Qr8GR!OaU+n#UQ%d>D;+ugdW2phbAUdZ{jb#coI9JIp(mCiI<@nl( zZHkMW_ZIoucF=TrZrA?jI34)H@63y)oBYosoujVTrI(`5PqwkfLP z$vkZ)4@Z4X;eGX^cNg*Q)DrjtI_~0q;wYWafwffCb*F80{uJxxe&g)fkl+PRZ)NlHl z!a1=GHT>NXb#0)gRn+?SRNsy8SPSbN)WBD6w(2b3jZ9sIPWQZ~Tp_QUG+!?_BYWZ7 z4)|6vs5G)cv4jD5Qh7v{ZtOn!MYKiC3|*%6R>dbuyEpKjViSFm1zP7po@;-wZ^z^! z@tn>f4cpS3D5m{8(4t~DCDoeuGgCjHp71HLs(au6^E;k_H!hv7|M@yjN_mwer=2G=$7V&%N?Zi{u7!7QU zSz`@h^L*j$nl;mNydk@0cA!tIh>eBucH=)lhfU}-{Dwz~QyY3oLN6leSS{2HBhTz+i_)^YhYG4(g% z*LS`7`24zuwfGwF_nY$TcXXlz< z^(r>sJjra;rmbYhh2|Vj*>lpn)J99mr<6U}@APavx@0BtZ9}c>oGDp7 zJLfFNx5(D{=ue|((U+l~{R!*xhHq)Z=vj1`^lUx#uG^(&sfX1^Uv<1+$201;8Tm;b z_Usn)on(BSK9TWt=tH%wv)31J28;Ialr#1x=tJMhtFt<|g0_&etTl}^p!bd4gZ`R9 zO-8e(gFbIX&uh)%F4jsG(r;NM>lG8r_HMyeSj~I$gJ-al^PU+@HqBlQeCfI9&1(`P zo6yBgJKmG}XL5e?^ele2*R!VXMONq7wU2UitL%I6mfyD^ow)=3>f5n;!9IUEGIy`}TO6z0P3`LL5M2}81dbEN*%eza(m%!i0 zc(m8d>?s4!LyYU==$^&sQPGvzJI(LO;!AXAreGXKcao3Me@01{{9xIZ+u5gRY)f)7 z`p;*ubSE*{M72iD#Nvv==SXzdngbSXTz82%%leDp^C;0gJ9TCdKtaii1obY>ZO*0McT zo|Ch2w*h;oGh^)S$Y47YJLuUGost?9&%^tJ%H0DFWldhZw}>{T!V})coeJ&u!h1>fM2p|*;aT+~-nt3iD#ymvzSy7fe*a=@TJe>$ zadWmUXU99+_WRT`&H#tnQteT1-`-T6vv1p`C%U$t6Lmc@CDzr83{XA0sqpjAEs2q7 z{60WG(`DZ}+o=sWK8@`(L%sxX%m9W4cv?8opK8m@sEI~qAV;dpq8GmhAJ;K1*@`uc zrG>N7@50y8{{K?!@m2T-9{^{Az`4o!0vaoMZ5MBIczTHUB%?MUiv*uw6I|LCrSZ)G zPo-6rYgZk)^SMV z4E>v*vhFT9;rtu?@pq;+66cqH)AWI{vio%UKVf_69g@}g`z`z7g+6#8!<;oPgnpTi z8fYv=-dO{=I0sd;i2Tqtaw-}s**}L4M}910jmP}nIR)7O&(vPWyYP(OEkWm~9(Qsc z`1k%U^x4Y2>&*Tc8wav@wfi4U8*j_Cv10~tFxsfSkZZiFb{lz@I;6`I%13Rn0mgmdE*uDdk@{E8l=>%UD&zY{_W7}{Fk`j$&`a2Lk`YZ)XiM}@I|Pa25Pau3ZCrjw10j`J37C zG9C|e*k-G56M8R%^$$Hf7w6s$Y4P{DZ)g#CLt44Uzb9UIG9-KtKgPaCyWx9Ra9w9$ zY|vb>7pF#J(0Lnv&Lq!Vd&IW)nbz-3hF!LNd>7NU6T)}-aZ!^Z? zIFy-Q7s*ffdRpzHa$oXawuX`avR|D1FA?0}C0)-wXtP%L z9Q?p)*C?u^kqN-+Et^4rL9#D*cItZf^y3*M1 z8Ep58(O1^h$rs=plD-?DpBwqRk9!Z`6NUF$xSqs^(jLhpoHNBaO?{iSu50(4*M5y0 z(RO%(IwLI(@7US~Ppa8S4(Pt5!#i=a{Og}Q&){9}3GZ4X<9X^d*BZRHIJ~zwyl3xM z)*HO{an_S*zqHigoinve`KXOicKSYZ|$h$%0n1}Q8VY_}Fk$;j?p9}Bh;AFpgeQG1P z4&nT|`b3Y+@pdURrm=m}_Crh;T5@9p zADz*LiHr^ZGZ@?F)ekm=WUBE8(FQ=6>kMxBW-swSA5oL(ul8inQJ9 zXtLMQ~&~TwO;g|Xi{+sp6}cRO>We6N0YsdCbRdU$-mP+VIRZp@AU%_ujit!cvcBFu>dXnO+2KWkH>*@8-QNvf3Ry@5nK(FY~zVM!- z*R<%Uls)bfK9whT;+pH*cxQaM^Zx{XRG&&QDvzIyAr3r#WOrbTP%uU^+v3jQXM^PO zgYPlt3x@pst*`pLjNcuW9?9x$&LbLHAMZN&aH+{(`j6^Be{+V&(E4WX&FMerya9H`Rao%mJEZMtYV#(eXV!*nmXZ0@mjQRIjdkODjAI{VHs*SuKO^ofu z?r^s8m3B@T`_Gl@!slW4d5F(p{;Fkb0keFO&uES+5+i?&JP7@dHRe37T>hQxeq@fR z$y*1UH3rV;u=bf~4U0JH-lmCh_8PUDwXB+mu~4sBF?99U&X}LazQHcuw_s^P_VK>t zEy!tfkL((3B-Xq%cIx^v*Y()MO;NNr$)ENkYX6dC)gJIRrIfjAOFr)FyeRk6!?cpT zwJG#X-CyFhC5Q2i41ClP13u2kA-Am=T;=By**wC0pTk}g4Sh-YL)Ym07x=Evuer|$ z3-CG1;qz-JfX~Jg!6*IVQ)9pX{|BFQ1ANYLc>J*YOcdaA;5jIF_XB$xRc$w$XKVdAPp9$j7nQ$b=rrE_3%={QQis>)`5p58=Nw+Y z>gd$RAS91peA4E_RZ)lSwMl1Y{ek|atlu#X_BnH|efi-vtgn{=`>smh2KKFtYZvg# z#!}7wEj5YR(}>^j>{7+7OnoM7y0DEZY{0M$y>nAFHWayyL)gs~thXH?uW@i<*;=j7 z4PgULrA_l2JGVwTp$mT})u?(S^*P_+7wf%_x@OquDDWtJTAaCS=()bd*uS?R3mMx6 zb3V$9)cVi8yiPW3j5*gi?J?Hi{-ZxiJxDFQ2cLL%>Pt~{+R+!@+jTSmJaK|m1G~f$;hE(W!%WgCU7O6z^wbA$F$DVhwM~-wbp^^c&~~4 z2hmLe`&D#THO&t9utkn;h~snJ?qj<(pdqi%uLjm|Kb_ZI(tTCvt{8M9-B*k5 zt6|PVT9zDsZvcbzRNuK)4ro7>r`1qj9h$2Ca{21&uM8Zrcja)`6wW}Rzb%zzd!JUm z1N~|5@(gqrHuth;m>X<$b9Mu^{IWE8)%qR1k+ouYAA5Y+O!A=hEb*OraprCzvLlA< zP|oytaznLu8{qL|4f|@47qt;}(E0OcwKX6wu4Y}l%*cymR$gr3Jc(ZQtF;SsCU*X^ zh}_gcbDl&sIfm8U-_(8978Enk+PRlC-_Ui|8g{TZ`ET@{xPBYs&_3#`u(5iH>+fXT zg1v-s$ksT>nz(E!?E{K{Gm4lO{-`m@c@L}ZZHm}Ci@l)Q0qIHDlPjay@4512bB;us zb0mx|FOZq{(YNvr8|kN+evp-G7h(emU-gV7jlGtjzb2#SuKZ7n`%^!nK#bhP~5ziwTL#^ zcPB|Fn^_AVw?E0rLOOv4->v?b<67qQMEje;JEF_Y=+JTdLvI%L_dDu~bqLn|=k>A< zVQEk2XjmM-k^Ay>{cq8KFEM}noX*p*&sFNWJ5!^VHGoFilb-eW)8qr?>o{xEj*=7R z`MMXnCHH9zU!yxZjqX4ems8Vq7rFzUU#nbQbj7~t!FQxGrZ$y5heYdE$-laUCDRluKBG0vQb_={KU-OX7d4W~vg zcGX7nPJtfZzhv>_stK-o;B%{@$L4az#36XEI#y-&_ahs6ZgTA}rhRXB%dUAV{Zyf2 z%jlz*vzIoXM|_6QNw#+9eWUZ(0WEqRKl5zs@Yx9eJVP~oWpuP&naeb zDenX4`_RK7+-hrpT(Uvlv9x0S;C|YcY!)r+d7VKiJ*#uY!{^4frMl}?`On-EjXZ@d zwVt_q1m0KtRr5Uu8#gu~dQ5pbyP3~8_T+qU^^BQQTlYaY_jB4-fb*H)d?z@60-Q_s zuS7SD@>$8}Ao$sctbYc)UmaC&USJhxjjXx1f-{;-tcd533cRSb(+$XU)tFb_-!^DpW6(NCX1c|*>ecrVC)@)~e;1l=frk1R z`yS}|nyORw&OCL(-t%LLk()WwcV=zE-(pH@BN%z@?Cswa?K{U zza@HczOM0(uGzDxgzri>H;RcD;2t>oiH-TQKNQneJSd#Q4s(vcjMU`M6tNZAw}GwL zS!gT1Lhhby#TCS}YG<68(z=pj-rDOSyHS3Nw-=u%W-rb~_g(|OuLYm76|sN&ZYXXm zYX4j4mm6EL+1QHMh1iO~_|7|JD_S2zG(Cr$&Q;iqgJzw;%Rx)qlE*(%{tTa@=^vQy zr|`Y1`#JNyqWh3O&3D<9Ro#Pp*Qd@F__3}V-=wO0clMde?te4iZEogHz8iTMtW7OE zoA+GJgzq31v-YL&O>+6fdG;myX_QMG+L*QQN-g}<06$&DI{KfmrZ%6t|9x|1b4OT+t}`xaOM0ajBCBuI9FrC6)&5HBW=Lt}Q}??5{3HgCBM@ zIA1h)^Q2VfENBlON9&ea=k_$%0uIR6AM3LqH?{XEjqEfuhzw93^$Ghc$O8NoOM}4J zd585^65897^;hipM01xL`mi)NoA0yC?~?9IJgrn{KF;g@G3RqgcP<~tKj>mCh3CU* z+~oCWou@PT(iN#G#L*MfE^ftNPa~63uZ*rEHu$L4%_KLx+_rNtXIOrvfxF|qDakM& zj|%S}>s=p%`x^&)xc|B9&4TxLvc9+0qc>_@1%3tIp5*XZsX4$q8FoMhk4ryFCk{=D z9y9%6(^tVW4de7By8s=Z*ViFG<({))KJ6siV;7kKAkf@ zn79ARxSFwhWWy92*GzOv;kfd3y{$W$uj}2~I{M@HtD*;!$PoGa;s_Ra z3ZBB2=);!yPv}+aQuC1+t%1zIui|VjY&c|wx7T(f2S%ODz+N-oB{RyA88Y;T8HT6Amv%!QTrsv+mkd-rmF>@!x*g?7$~KjV1;ACkU2@02*1 zX4|>4-s*&AePSd1q5Eub*ROEDujf-T*7M7L_Bx8!-wm&4^#Su7`u}Ea0AD1_FK?ce zS_r>f1yAodC3xp-*Jii9hMqz=$9DT&nZF zbS$4pbX0C^=CH5ve5k$TmwcDMa1Q_2&0%K|dF|tmhlsU=@y8fA&Bk6

G)Mq%ii<~cV6Z%|oOmdL@G)@kd9o|fALVHwdBhE|WPiLKO;p{re z!8Pn*s$kv^v@LpEHoj!nAo=8j*be3NrI_eO>@LAs%G{glz=996R&kH3k+Fh#Rb}bk zr-(%>*1MWmuY3~EFIEq}H!p6-9Cb#o*4P@-^lAAr-u>7YHAlsCQ+~`-_LugB+PTWb zK6g2LMCWXnnAk*eg`r&^rw#EvlUQ$vZx^SvzBaa21-^D+oVJW>TUv?TVyEJJcNUA& zt~$;bK0+Sv_&V5*1)S}RB6Kh@SC7YVtk_B1e`{){_Rtb5i;0&N-zBbV;=9a&;=7;3 z1}U`J-+=6KHhWL)X>UrI7%y?SVlm!L;7&I8m>92Yb?hV)M`z*Pe2QQ1ZAEI7{{GYkh#CftZdaM?CE}#2J{F6{twtod%0sA4tJPSv2 z!B;J{C5EV_v8yIZ?Lxln!go$z#<^z9wd7fv*w>J3@looDQj_CN&_pfk7pCThqqQ`4 z$EyE$#a&0nV?(C#Ia{@6GOIL|IR$&tlCtBrppdE(`Z*W0qaJ#E?j@oB|Jus1CI zy_Yl7>^iPwqNPFioM|g}24=o~%01(+=WLQFuN_-&q^kR;e;$kxpE->`Yh>d?GgGy3 z#?qh51^(j`9Sbn}{%!mww0FkBINmcSg15Igo~tmtR^ENC`yF?`bwA|2E$lTF@6`u7 z(%U;rOnz`~y+i(iuPdEFJ_#PPDp6du~oV&2uS0n#U)c;*w z#Q)Vb(XH|*JWef5`FO0KFPmpv>|S`SxPG*4ZK-kio@9NQZB;KGqL^w4(b<;on8ZHYoM>W#>X`gLE!J&dG+5+|io!e4dki z8Al&4G;%I~eYt_WF4Z}`2^(Zr+rnXQ<3B`x`+VlY`xRsaA;MW$T?^ft_lu+4>XVI<)nBtuI_^VmlV!_IfDS!t3t`*J<+CuHv)B zEwhwpvKAD)pKAa*tneD8o}jsFwBV;gxw4<@n6 z*h~H5O5!{*#@&+O+%NE^y0fdn)7_3{RcCKM>x0tUqTAhO9lOHBZ60R)qYhUOgDZU( zuAT;0g>=h0lev~puOXgf--Yz*aTVHbUgr7t?7Hl$0)4=VLpW7U|QP~tvv~z zn;pFpAI|3Mh+ezE`<38%OIvc-$IUiFuZ_$@!p(!)P^_t$IVk2IDR$=lBF%yPqGm_0 z@`)5XlRu0k5t7=NKWZgjXVCLe4*N+G`Q zF!)Z&7JBo0O?>GQ>SsnyJ{0oo67fAaAP#8u<^QHUYvc|#Podl?Hjkm)X~w1~DtGXG zW@48vGV_?rYZ<#v=jmGO9-&R@aE-lr6*z6-?|lWhef~@<1B&INh@Ok#x6#Q1gWt<7 zey{uQ!fz3|{u|>LJ_+!f<&%@fFLZl^{EP~4`wKop+P(ic_?-z47sf|7=JeaXHN z&a0)?_nq55^nby4&MFwsw~CDCkg;FJvCrYt+&o#kv5s-II{RF<_}_DOT^t*Ils&Se z?2(mC-fwL3m0#@tRfJ6*4{Y*y_ebG*tyhOME4{h|x_>=rwvWBe{p@vC9HXBzZVG?r z)3mj(y+4w#ck}OAy0_PMlH+A$mb1qf39s1Wf8%VMFyCv8{{vj}Lwaeo`rvzWc?NrsiMx?!RaKRH4@_h&9C>yK8HIc@ z>v^nWnD3Hj6|6b$cJgf0$+HU9oOR91FK@4V|0W!}$6vNSB>GcpPAB_}w?T7p_i+Ed z#j`!XyP|uM$G5Y?s~jB{vu}#&hm7J{_%BBPe!LaPpBV?j!*m1{P=d*@y+^g7+?EIj_+6G4TZXXU6Jv15pyckf3-P(sx#SX z;{=iTg|#^ON*&O98Fu8|*!l94obg@uf`u(;r&|$@^{L} z-%$+x5PtMM*pXoj9eek&6Y0yx1jD|X$x|DzubJ4>pQf+MCAoP0)9CgU@Oc=wcmf&u zy2LF^PKokcYz!@*4i&eM9~j2sy$v(|y?olv_Ywcp=8J8+mCt+K_fPP74}T#&oZ;x< z&g0k*8FJ$V`%0~cKV|9Rj+g%D?V-yG#9?m~X zFJ_$rd9Hl*A@VbJLEqXNDBpeww7nm>9b>KAuWN^Sa)#a=-TU}-;kxz`;^8g^om&^w zIyviZcUML4&Vz^b{R;Z81fD&>Q+aCD-l{5KWgR_@FRZptS&VNOd8 z{5J8(E9+wZ4DM@*ow3%ES)4HYMasf^H*oL9Xe6Q7@4RWY4s$Y>FJJfI@nXNWrcOnS z*gQH=^i}Qj;6e1DzDp180-vlgnpl$8gS&WM*M!4`?1?ur#O#Upu{`mMr)}HMLF(}7 zJn1HMv(eYrL?(WpW^A|ql5_a@oDXhH%uBdo{Wn}kf0*^1-HNHXXJiX?A(z5u4(w0N z-mRK;*VuUDU0P3}rk&T*mVaF>A^1Wug`9pK3$xchmj4y^4r9r-oerIm!#d90cjeZi zq({wK6#N&{T2wrywWxiCYf($#1Fc2PSMJW7S*Z-MTiHArVv(BjFn8w=G&E){3L9yY za!AP6QM|ZMYf-ERsK!AZG*-CZplA*m>lCa-?K5jpz`5a8yB6hT9yw$^xg0WI_ur0P zYf@J!pUbUDUCH-3{56|vCEbnYv%I^(eO}={FXJTic9y4`DQ}1#dx||OrHjGB%q6^`wa*&KlklFC2YEbSpX2$d4;*{__-Wcx{+e*V-}&%c z;dOl%udjmFOC7KGI-7SD&+D3Swt)H|`b?VoAeMhDPuX?bA$a_I)W+F?PB!z)818tH zJzRW>mTSP9_VC>W-%bH%J}x#$-j-|t)s3^yl$zYTyl3F;)$n$Q)`sC}PoH7``MTAw zT@|jKmm-^dO=o-0^6>Q3=T|`c@Q~J%-TLtup8XWG>v(ow?JCx&@jYtAv+%ATXFcQ8 z9wEh{$M7utrZecW>&J@Wig)4LVmy0{^<&GktaJa}pJdmMdych!{71@5bG-UtzAJY6 zA@f<%{Rie=N%uVSS=#--+~;-f^Y{1+dG?p=!&Quba^TB_{+#ydh50+RoJ$~Hsb%eR zYZP0nTn?>!o=+T7`5GnImo5CpN1oTqr)pM-51Yu@IAH4g-*fMdsBEo|Ioo-^a#%Tg z(BwYF>h!6Yc1~{R`&-&?pq#E=@P&@0CK2ZuINz~DH5uldn=&!TAaCZn`@J5u>z=l4 zpZ8?@w{bnz|G<&*XwC?5m7&g+@)@@}d}Yq)*#6A`XMTM|etl@q$H0&F^JsrnJ#?h? zsW|v3wx4G~R5E5i&--TC{X9`@_nAwN(`tYo?!g6kp3aOG@hi9)IC; z^1BAf@d;^QnH$qT6&cSOZ1wTR)65*7?0DYDc>2ytjP#WZf3qK$6%%h{4SfnQE9R{@ zw`6iXF+Iiho`J5C;A>Ts+s|pv$3*9^`}`}}b@6bF!jtwg8+r=pD17E2aG*0$zNmaB zaI&EQCvS5&`3Hv+KfhO#dl2%&csWh#+xcVrLVqmGXVp;%3LYl(GXP}Rt{wqc$>Lo@vKAf=wh=+hv#KOKh3*a&=C#j zH?5zFKcD72`f_+_7ddG|@Y2xP?DY)x=tSn6kPF7_(fNzic)H5+RAp8sRG9Ts?T2ZD zZo~s~$Y<4_yQkcljL6w%MYFYI=GC@k`M#oi=d!VUzd?MT%{R#9%l)t8z;|p~@p8f2 z!=aDp?`b`-qWd)HN1x?}p31skQhpAr#4j2f6a9f4S;X^qo1+rFq!wyREQm zyno{QSN%)%>;HLrUsI63DIKSpW@kqov)9w+U8?I%U$O^QkY_LbsoDan`!EGwras`v zRBVL7v!an{*abt%;Wo9qb7y3HFcR+AD@lGqSU*@}^mQG6Mh-v?<5R5&Q`dp|4YFHx zwiV}on|ct_(ATno`k}8O+M7mg2=%FU7E`-+I<uA z6Yk7C?a`QI=k}F1KwH|QF)105UA=gTXgjej zkDh=j?{@&lj{-Of&y$ib6UtNZ_!U1hb~JjO+$g8ljr^H-;TZYzDe_@M{G=USe_MTG z*9-;n`^6`1J*`8=A2~gn!{5I)8X0_m7zFh}Dr;oVR35$>zoY`%siem8q2+DE2dIHe z4dWMU*n^yIXI{^YUfc<9V0+C@L+|q2GR$4(7V8gZfFnJD+#hO@|IsDraA*|X{KkCb z?O;1~pW%tE@W)Q{-Ui-R9N`IUcjft(GN-GM!%vRBvQE!VLoObO&zW7(Hf`2MXlWC) zrJVI?=z34v4^un3-tma?rl-?CHIR)hR>%Bhuvbl;b=US+XRCP|E)9Et6!Z#vxwUF+Gi#@j?=%si%)fygz9{E0KX?zb zo3=J82i3f9YYeD9+WdI*X#YBD&aiixJ?O^I(morVb9)M~*u9GF+vZRsVO3O_nkW47 zyy6lj_l7poOTpg-^ojgbJ)0;IrG6kjz5rZbr2Dep)Ry9X?c4q%UZQwI&x0kG=G8X1 zmUra4^shxeH3R>Be9q;QIa&w}FM@`9@vWc0-juv#fBo83%NGp~Tua~g0Mqr*<_+M7 zxKm#Xu<$%I^5}ftRZRoMWF!|B@GkiqYnzrQhx@JpN4}l`aDDm~`6^L&BCno-!!L9_ z?^MnoKqln!J?y=Ah(?%8`7Gy~@7nKI(LICjUN2^RZ39=gNOt!#>|Yc=XMBAFSGVYt z(SrIoCLb(UpEC_j_XB$$_>tT+G1Ln$Obw6^>vP2qVCxUFe={Q9t=hJpH~X)FKjAUY zpMCJ>5cB8x^C#3P(=%G5GC9QD6Q4y<`P$4}<`n+_Uu}IbFh|Q!d%`}V`F|T zE)Gz_v$~c)H**bNSUGCHKLspf<^vtBxmZ*<7iT3#Hn@2xv>&X!oIf9af0tQ5K4{l( z!nydo=7RXFYNk$Q+}NrqGj_&571*bN55az|K&u z{bkIPnODU>0y*=qtlvw{Y~|f);M&yM$FDT~VmpX$w!o)bsIOm!&FtSTrw0BYJe;!| z%DNv>eGlLrWd3@osozUYed<$;Oa;~&!;j?tVW&@}FY++gK0LA9v}ev1puHjJ<^XM~ z-d?WVithV;yU@)Lbaa6BM`?eQ_D5;|0CA3omron^{TMufhdyxQ`(n@I+G+6Q)|zO$ z;uur;zJ>K8#i@m}1zDVZDyw^@c$`^U-REWVy$)v&@viJJ=@^?^SjK*$l5U-&@Q~up zZBrgcHuOX_=NH6yEPcr~9lLkW-qZX|f&TPa7to<-Z`VXC2gb9PuSVB}{;K#<@xO`L zb0gi_POz8t8*0<-Wi33PcTYk5L;IHo`t2D5vV&z0OJ6wIawPe^OHxmK}l^$OVVLfz6z?!o0KwI2_oVnB6muJdnRx zxPOv8&$E?7DI6M^u)*eDggER}tzF4ya3%RHnJoJ$^JL@fjhyGW)7cn-4b>wX>QAVj zr+0|GaMmWaRa^Zm?dKZr`J000t9jPjTk!rq+3lN{i#@Ez7uqKqnTrhtbFt|J=VDWl zxzM%zxp?2O{5Crm_WO-~E}#eLOwGe<6LNZTb1(<;=d$OMUZ6S1Kc7@~fb0RYPlWXj z^kh%+)pJsF3TXKq%)^HBnTH!BPl&GvvKjd@@-TQ%Jn*a79-ENO8SZZY53>LI;7!Tp zr)t^Hj$ED~T+@!uKoNeuj9KRJ8_MO4^5rUHYj+`!cOj2sk&&O~ogwBVgO1-pyE=>H zN^H0cdRcAje8L7~Wdr===SaMAXd3&zfs6f}Yj;kpkSyNk&rVTW2e2Dd=gPNvVbJCf zy8QsMK5Y9SID2OK^kLtJctriIsn7Mpx&m^awN2!(w=s4ihqLyD!H8lU1uu3;0s z1)1K@{rSKqe?l-Mv8~e0)(+CA^Aij#D`KK z7trS(-xu`b?S_+~&#$Py!!hVnHr^)IzQ&_ZLz~cOCZNwxt4-+A%olBsp-*z#M4$gv zoSZifPFDRFaq^)PzzKCVIJKNDpd##L9Ym_HS`}$=>@BZU2zJf93B7{QV1m;`4M6gIRuxvvDRRM(%X=ISPYM&pARDE`fuS3 zRCqjgdX$pgBinX}9K}!u9a@}|K?kAdPzJ38muf?89)z~1B7=q&C$^Wj%|Hf~Ss8?E z5zf2}`ZM>A_O|0Ij>vCKUmuMOdKt9T%AjFnXg~Lj41!;>GU$3IgZK<(P%G_Dy)a5C z7E?_eCfh$UPW>wf)Wh&W(SZ6#whbazOHuRiej)nA`Gxzk ze&MwimcJ#hrjPIVedGFt-u`=i{X)C0YU$8^e{YtyVqL1~E1M%zCE9A_cVgsY1^#%K z$+yj~m!bMPT5HYPd&4%TDWt2(&{Yk2x8B|}u`+V|r7J!C900!urblM(N~FWi7d&@n z=iyf`GY{Zcev{Uk;%(E0L*3!+xetQhW7%_($KZb6Z`+r_1*>Ddf)Hb2~V@%RN}VjR6MlS4=?ut9jq+Xy?0;S@ZI9}G^>-yu%_S~%tX zA@uhF@JdW_cp7bF=LCG)IVnHf#GGW{dBq*WHa_dy5RY8o=caaeEAr6A0<*UEFUTqR z@1Gl6=f={#tLYQWjgPO_yZKRk{R@Gutr&=(uVVDiUf-h`>|jU#J;_hl8W*fb;|gTvgy^Vv;JcO9pGi+h94(e_UJSpP zTeZJCyT_|DdD>gdp1Kgfud?rU41S$0o~7km+1Guo)lWJ3V&r_Me9NDV1oGwD#CGJ% z`u~0zF_wOB^Z3Q?678NX+!9v`<+s-L$H?z%nGfugjiF9_X{`Ld z7M$)-4nQEkwa5RMKHd#E`)bI@@*Mr14}SH&m*>XEyP?3xGxFQ`cxFz{bMF`*5817{ z1&V>G?xV49u%+bVZGe{*>o)ckIw5Ocp|ch1Ms|7o%CuWayNb~$=F-pj=A#Q1Sid(r z&r97rFTe&+46GHucRulkRu^~idU^qV8T(aPw^%qlhj{mTmm@a^TU+0iBd6H-f+BGj zUCWQVjMW84fy3}oU|(;j$l2FA!^6w}Lna?6*l#3!y8Ug$x;m3z9xMC*g}uKW+Uo}X z8W~@rV0_JPd`ZT)Fc{y$V0>}L*X+jE?qdI}<(O;v;~N{}d)ki=9E*;ZXlxbKKMi!; zvBnp3``yg=o*6s7ulx^>@3RHtd*?}xuhH>ov-S@^V)rwJx@FP-@c2GhFusxU@#k-k ziO1&ZHOg1XxP5TUA2|TCOr45wd~aub@Sc%FLl5BdAw#^+7kj7k^`0yp9&)lr>t+X$ z5za?4{63L7YzL7Gk{_}WmHQk;m1{E>S^uaLSt|K4K-Uv2gP<=5*OARgOy z1GvF=?Y}XXQ_wfT<`i7@jv%K1JKOYmBlv`8)A-`pPbT-^y}@(j4$WiDoOSMZrM~xP zFH>jJ)ml>Cdg~?OnmOw>e$Cv~T45gM=k!x&vURM^f0#Yq3FyMe7hvfFj{o(USJtKF zqf1ZV1HWP56~AuW6YZFhdRO8#?#EKbS7#2DYtY3z(n|}G^NWZhEYMmB{s4fvb?hdl>^EzlP}XJ|h;AhHX_(-+=netq?Y#h3LR?Du7!Cj6SoS(Y}=5N)`1 zr-8E)Bew@M;^phH?1f;TA9XUv%GqY-8Cl|h4IQh!*_O_H|AoOy{z*p55&hT6Uql6jv4yS2k~_eBHuIG*FwzI=#{rQ zy|OZvQm&qIFxIix+N}Fy|NU3i{aIhyr}d><{n%{$K0CG_k-s3h5Uwwoc%Aw!zV2_{ zHS7M!G1i3k*)#9Uy1Q?4ad%|bNv!+#WY_)CeS@@l0R5P2x1#&Opk3vc96(p*t^0$= z*LmIF(8Dy3Gi!TydV2Vb!&yE(5Wj(*3fKK>lye0Qc$x@lK{PaW-QV(z+L{L4>2nI7 z+7DgldB@qJyGpr^ElNUj(R|fo%1gIz*w}3~vrXO6(60G4du(41c{y;d);G^fJx7hV z6ZNnBJGXm=d{s?*0Q&Tw-)p1%C=l9yY6qflO6rFnO`tf9QL z@z8?&qLtJ^xF_0Pn~R6`Bs<2gUEHoc)ryB=b7bS8g)yLeuy36mn~R5jYMaOFhrz4f zH}Oz>XX2rcvkse!hpy!P>*a$N#zS*$T)6m7&MW(c{_^RV6*Xm}}^9YMZUyvV8Hd_G9!Cn|OqrV!!sLyf*QH z(dqc|<+QP%b+3u!x2xW+axavltTy|MZnyRN?Ot}1XVD`)_>GsK(Z$(3i>ypP%hA$9 zv|)0&$f@tU#oF1bXKC^@mjX9B@hRY3MefBaaxVs=Qsql@D*yLNa@Pj%PtaXGRwtHr z5B@2>3H|BpEwwj*FR-3GjP>MUtVgeuGLABEv7WJ9O+2iW{*`NYH|N2qUFGWUR*v2I z45)SnXWw84tUz8Vrz`{R>cCMwI8wcT)==lwfg|eI64N3V7kgoo za?bFlZojP)P>v>w7{B{?~MQJ}-Uzq5vY{;y5S z$#dDiqLl{G3VW~09j!b~KC|bUHh4yPzFC_WzVJ40Q`Y9CrmoY=KmV1L*>0^V%MaHx z_l2_6_?pOf*}Jvk(YD%Id)WUjy^+>+;43lkaef+b#DS0V)7CCnzF@coeqKTB@VxLMo4k%aGkRwZ?;IgA~u}?jH3%bMc&2IQc=WdO%zaE;Nr*(DZv{P>*^)PUW*Wjx>y;A|rjKY5(AeLR? za{8pZ@_ovjzByC;h1_>}eOdmxog5VLSEz5~+ZZ0>`51a;3ORmW&unyhM)90d^g$+> z(*qy)Bszw7GD*RopNpsQmokp^@Yg2BC|Id2uy*4_tGA?I?vZ|>J?+KFwB*{o#JAgm z-Xi8WSfu>}}@^j_A|c^R;|le}eh~*_zcCz*m(# zmcGcHOZ0cjIdb}<$9ymC{+j#zs`8O--}s3yeiNQK3+OXy-C#y8BaefL6&+t zmwxDl#v^23q0BkNT!k`cg>^AC3w$efeVJJ)yk!UiH9kcORXZYT|wi_nV-N zIQO@{dffvzv8PP$t8Li~C0yV9>W3a^ZksZzm|SN4d_;Tt-U#i8&udKF%b)A&<#}oM zFO?&L4me^yIseyv{>*%qc0ccaOHUl&d&moG9X`YL<2^<4smjnh<-l(I0pONDP{J58 z#Oy1ZWcB^U z4!5j3%C zLN^xqX1~jLhOh$;f_vfJ#Cp&*+5@cf4|OKYfwNf;iL_QPyePTExzP zf2){_*CthG`S%j?e{CC$!?I^&J7swHrE&1L1|AFTr+VO$4b_g$taW^*ec0mtjr0|V zjdkNRY#w3{^U>Kss`RM&)Kt94OnboF za+-ZFd%yFt)MW0r+UNJxK5}tN_3CAttNj{mgZb+B$VDmbPi>)=vF-QHZ7ZmiQ2XtY zRB8DBKihf%`8KPa<25(Bz2(r8=6jKFi=6)!xIMijG4fI}O06$B{cz^jqw`*>k5ZFM zqLFVqybE{w=k4jP0(uhudScOI8?cu(w;^pUE$DZy>-Xmc{noyg>$hXTtCw$lT=b#!>J`g3J&w=0H2tsZzi44Dfc{nep7fmfVX3L?c=ML*F9bg-s=K* zf0yr*i@^H=d;U%aUY|d{$MATrzL&@G4sfja$4kh$@ND*K^oMNOG&0TX6G86vvwosI zvV+ao^K)76Adl$awbXUJJ~48|Pe+~1u{p?tAI_)8oq`*B{E5eLe~F>TLL3`HbpLsL zUu|H|<>v_Y>!6PWut(5@>Jhx<-_Y^M@30<0TJ~X)dITR{>+M4;WBqGS<*%$uGAIUJ>%9rU-3Q!F9p|L_ z&?#wji*$PeIoF7cjfeNAbDwyd?yD|wGyIsOohyN%AAP6&t_jsxxj0JIO24bN5feR?GiB?^Z;osKIsJ8*dL`$jN?+aoO!BnEcIh$6uVjRb zMIK9L%?EakuO@16`{C@K&YPmLdw2cpj^}ofXLwKBO~ZFb{|Uh#RsUAupM4zokFu{${7?-3 zE`$FZ{a_P~h=%G*V{4PIHm_aqYU2aS#i=Gfm4Pv z;xYD|$nx%g9wi6I$V0`AW2p;QzOrrsI!HLydA*uD^wE*p%PUi>@A5W;qZg;M|4DEo zFT?XTs|x7-MB&|W9C#lofVUKQn*w;R1lBDV&Q5KG)*bBoYQK7B>d(f3eS?Gj`;KNx zR>hueIw$d5%fe{cOz5wqH21qp_QsCL-k-v`gvL&=wnhVXKr6o+(DxbHM{1)dT6Ps} z#8*YnCi&gYxq`$X=e2ZlE}TBU5G`xsQ~O%u2i-HLMa$Gr%gwQ6YOm#!v1RjveoVXH zrf=CQy#I`zQG3Wk{Z8=tezo`2T%QP+dGSYczpIEfw$O(1its@vx4gu8h1UCJ9~=n&46%#2`g%25mcdSw z4wih>Z*R9XU^B^<#K*I3(3Zgg&*ZgnFu#okY$x@p^HJvWzPJC<^lAFI!{Kr`AD4Q* zR{gEAFvnOUsb!uRTi)FIb|U^so_PIvs97>Gr$74^<3A+f%NEHa)i$+fm&@MOd4aN7 zPNk;qo<{s6)&d4^i_KOXtGq2aOL-KP+N*gzdf`FFOFaE1eBK6p!p~B>M!CYO57yN{ zeVaCFgF%OHr4DY(R12$~W3T+=!_e+ou8S;Mcw#L~9vBQRt<>?mHGmIwdy{%=@rPLZt35E|N_eQ@OE!#+6H_4BO zmj>Zaf^TPOo(=zjL-~B8QOPp&IW@X`-&!x?ypWUV`+fASelDkdQ#T*j|LI3oN7WIF zX^JMXP9R<6eO_xj`e&ayfoJQ8HBrB3Z<6|S5j>@F{4Q|!(U*bu1lpJnjP2MUGwLKy zRO=?OZAx0qD%90XzUn9+o?y}Ue#4wTDliIDD|C(slmsb^)ojE%$vrn zIf^t7tpB|ozCT2r?L}=m7q_f?KXRd#b{9B8rc_6819S0pLyIM;A?z4kAHruii0=?9i&EM%x*yy(L36Tk25*ne z)*SSi{b*)Aut#}1hp0EEc(~b17vOR5_Qu(2Q*j!t-D!`;G-69-OQy}j*gi`&zxIpf ziOH>(-;Hf0IQ1t#hV|q}hj_mio)oO@z)HL)rM;-SJ_xMxb27}s0buO~R>>&wqF=*j z&C}AeEB5cX(P{A@CYv9Q?f#E z6`Rkuoxpt7F`t9&#pm+R#(`_FJ(x>}D?gW)0FUTGNqUDaL;Z=Jnv- zSJwTSKWt>y{?AEe>alG%Oi8_~Z|t?V7r8d0$hCT|NvwU+rmPS{h?r&&n@WLramyVDwRG_a7?v*Wsg|cL&g|qO%(fKic!FCBL^Y z9_2lJ7Q16`E;0kTQGuL3hDdgh?pNH3=N7|j_pm>G7w;Zn+y}lD&9<@m zlJ1|pN*$tjbV(Z7q+Eb%JmXXaHDm?H z8f>+D;A852r*=(D++V)rCVL&ZcQ@C|u><_LvUq`~1k*nPOTYMwaj(`~2QVyvpAXRg zApMuqzjFIMZU-3aAUIbW!j)o<(r>CCr+sFB+3tNwJEwoC+$?zKLHBt-pDFWQHb%Vr zb9^V40=$+S{y6w6Yg;g@7a9{?wSdD-Tw6q6t62~FPx{h##kRvf|Gc12#Uj-9ZM1Fa z<1e<|!S_P`IxFC>x6!8f%K!861?!*Z_RHgsJt{q;*tL!8#Je|<`*Y4H`yP;E3p*0C zOW~Ewbo3YTn^s~wijU}-GkB(NI_p?xv(KzX=O?Jy%c@d&z(XYqz+tH8Z;5lpGF2p|=9i5l8Yx&(p40kuS z!U1dr*&l~Q)9BU>`h3*d;`<-RW@UbwjuOXV9Lgj1zLAxe9&Wo9upj@9wV-*>*JC@V z%cy-XZ}=E%*ZBD{)>4Rrch;U(=k~qW^%1pMO1{=A_P?0C1m@Pt4BO_ksAMnl?Af7s zeD7TG3TM!l6K9g#l3n593Ew#pSWn~`$!g8RYRv<2lJx-%dHPWfQn)@YyGJn-(c-4F zi3M&r%jC~&crW=A#Pu?oFMP-TdOL3)WsP~G&LFO*_F);c2VEH*b5UaCwo>w*N~1?J z&{>)|TEgV~HKevLaWyl+X{};B7f-P`g~ydQbvw0{=96==f?9`0-a&WL4ZmisU%%_) z(Z~STl!Hi)+KAS>;>r^wA2CKQd2Pp~rk1g2*5cL0ji7B4lPb7g>{=f%X9Ac%Gak&< zMPR-Fx#eK~u3+Z(Zw#}W!^v(A?{{VZ_rgX~RA}>pkq3e-b z@}t(j8e3KiKY1GeAY-dVZi&WsYCb;s9y1@{a$hZUEn3Ur((*WGbk9~UiTcAwd#nVR zLVKO!nMSoo98B#sif29)`SyCoYj1~Yg?2Qg-o?9$JF6CoVmul6*T}6XF?;>fi2Q z?Q=>kXFfCUeb908oA^n**XZ8U`srrg`=EPo4)2Ku6TD|+H+0yF%^;peSWk|__d0*8 zH6oXY`slUkH-XEp6Vps0@ zDb%l%Y@V|)HX9s2E4{oKpG>u5)yC80wBtd?Jz7Q`=Pk(CUSeYM&qY_-10g;ae@rcnE>-Sp8S5V_EbsPsJWX(T z^0i4j;8mlmd0#T$@}{S!#@P$bh%PlS?>N(peI@V;uXm}&Hu^~Tc-Q4GuS=XZ{J%Y} zG#>T82v`TX|3={JqfX+2D7r*&DpsKw>BpdZ^;yMlWX`_Y2)f2E|M7Xqckrrq44s3g zE2G5sSA%2qt$5>M-USbTzUA`7b3=oxzmI>mRJbU0xbQf6x+1zn&m)WG)skx|SxGE= z@2XeZA6ON&-ycB-(z)RnU%Tw(bse$cZ}uTasFf=QSSjdLQ%9ulX>1TH3ArQPH`tw>-$$H14mH!ytOs@2#w%D379c0y!0| zsZ5(hJ_Is!6FGGok)zN#bc{~{EW-6<-nTZ5>{);IvYoq?;D4p+%!4n{OdOgqac5|z z-_eZVMD|>wIb7LFo(X!>!#nd7YX9e|vc|we96~JDvQ>Xbn3$?fCM> z$*MQ;uGjDLCHro+`W@LfuB@_n2+vHnc+hxGf#%cfwR$t3qNj2`g%{&DQQKSkcq4lp zjo*|^%>LYK)~+-DQnTIPJCoSUYm;*Odo%b4+GlO(X2W@g59WRC=sNM>256@QIiJBE zmW|ra*=6<6&V1wow#nWmXh*(IYgDrKC(wmxHqCE6|F8VM3)wB3uZebK)3@Mzw6Pz- z=%%uMf_(JJ8;d>Q4=H5_nG>RO=#j#tdI7x7VtLo zMei!&nEZaYlY29;6<^^VwjQ;}=E+whjD73FGk131H14G=Np>ybQ*_#lITw3;}zh*(1ROiDBE6n4|<_u_?zaw=7_O}_ZyIX z%FpVT48dN1h58mc+6(0A*6N)T zqgP9drQMU2{{>yv@#*&9+^Mh35m6bpHF06H2XZnX>D#QBN$?~2OS4k2g6uNh#_=Wg?v*!DD^ z28M*y!S4o!n$qEKmP+mbgJNIJ+M|R#nu_chr0uQr!P*kK@1A=P0M9|->BWwz!4@3| zU{W2?gUGgr&~=LI90I19N82Bm8B5$hGsam84yJn?Oj<*c4J8}L^Wv79s7>H}G-P;B z?WZfVyjY?=D)3=`Y~SutA^u@>yyRFNHbK)wp2H?k{Nv5y#Z%DB2FE3bKTRHS8eY<~ zvKxfsv&Db-KKC8&I z{!re%`Z<#q(rNNSZUE*Rqmd5q9DRFIsu3C3w^?V=+1!r1ZX54>AJ;!)GyA+4+g|E| zj5{{QA$|?VYjeX(yBpzi&BFzJ_QfZlPf8D;LTvD@OBT#(iCKMe4YX<3sjAQ?rQO53 zURh`2d*E92X$?&xca$&mJZ&9DuNj+?`F{V-=-C$5AEZyj2Y&`{i{?Jbr|j2S`h8-g z;!=wv#a-jwm!AZVW*-MfeXKcXOy7l9b`u8~bvp7Y#$;lP=unO6NyenQxF!$k8pg#~ z6x%xS< z2=(#8YpuXGAK1ccKA&f{@KC|}3OJH1*T3=bAwSXRSnwixmHhU8_h*TJ_Mbsq0y}4r z`RXMnp-uJ(c@tVIkX^EYIF^35@+sNY%UWB+_ZR3}_K5N$?j}dWte=%yn$-0)_DqGI z$5!c(t%LrMjnhlsM#QJ}vr4)@rkWn|HNl}`G=0;N8E0Vo(Z2AVH~s>=8JiWqKzi;a z_JoQyl>7W0-u3vf=l$i=M;0e$4zU5L53~O zc7c0V``4kvclXhj=xsjF>wBGG;C=b@3wSqQE?QdhvD-Yn1v#AE0iKGT_h-*1&&OA6 zP60mwgT|jE9-GHcT-URPpSXWJI32@JmlxqD@ap;LJjYLonB}K9_hP)`+q@r~hhuA{ z&-wHjUh{l%i^d9{>~Zw);!1a3(uZX4yfK?YJK8>h^^%EQC8fmah&AH}KH7vXG%=Fa z#@R`DdlT{`&b|)rJr>6|eG-3CpL%v7?}@Ku7wLDBHuPKkwvu+VUst}d&O?mX$ajvf zP2War@M7#jVAnIc)_5Un1y7cwj;_adx{Yy1z(d>pmDYEy#wHuQ(XGSsoL`6iYzZ+B zeh;GehWPATjJ{2>PJzutjdOga3X>D8by}?lY5%s>ofX|rxjm#G8AMh$@Ln^v5Hix_ z@S|%E@Q!TygS->P%CHaO-9JSxORx7Q)gGUV`Q+LDmdMzRg0mkuH-iho)y`T=2lC{j z0i1$y2pD&wGtve|Tc1^W+QW1yF!j-Pns(JrhW@5ux6Q5S_`c4LGB{#=z|0x3UG-ZG zKbHmlhWL3HoQL@7XRNXfi{WRo@WYq~8E=T6KHgFN@(@3t7k-2n;YWB1@N;>LbA6f9 zEzptn&@a*_boBI@#Im5ZL1GZPZtink_XYD-=qY<08uITE-x_z^!}L{*F39^y9dI~b zfczF+$foQkF5q$hL+VT=D4=H&qMB0ASIh8L=22gbWsz@wUr zK4<{GF*MNPXyE?@FnJo7&U%BNhd$uZx}fU+rm?L>r&`1CaH&53H-K#j*!(=Cu`4pz zOyOL_*=v%E*TlOoJjR>|R{a&D*{>9V_gFN0FR&HTY`y%M_+OZ_I)q2zOM5MA*<%qA z+mgNh5p0j~V7RRa48?G!7)<7(;y7Dah%;>*-sRph#qL9>EQw5w0^PLlm*#40wi zw%Fw2yAwFuRWRdQ?(^~8zt`D!aT^2Cy<+kv7R;B%rSV7}i|;~NXXJ6fbFcL0cy2em zRuS-;WT5A@v>TU)-Q)Fc@G6|PB5R4IO+ElVJuYX!V}}gSSDH0p@mQMNihSOx?EWz@ zh_?=TJKgmq-a3e!JA@vnFW{{|JPw_dEo_{Pyt=dueK!>uA$Z5b-6NV4a8{_xHbM)7 zfy~nSXh;WN1#ZuG!nyoS(WqqAbmsoxS&40rv`wESog2F_Vf2*dZZ$ZQ9u>}3(YI*O z&(|3JZ~^>v4LZW$&Ytt;<zmgruRAmM-gBOF&U2p6bDrlp&)I>_hu-S{X z5}mN8nw2X9l0vv0l^t_!@Y16@1F`UfDn4N5>m|$eh?J>Ce8NdSZ|x z>}?WH##dw0SOd?l1lzt2UpnQ3^H26JB2InTna%3U&Pso{6MpVuPIoWndFi@~>=@3+ znVYc?^&?N*_)S-q+)LbCjP-%?T*TmQoyn-OqKj?)w;LQY`Y*b2lz*)|6sv06UYx!@ zwye3=pC1vBmd|n@ zW6?1O9~qbwoe7%FUg)fcxaY2i8ubp} zP2cP4^nbVX3Fz1kMpi(Vy9RP&Q|bS9#m}DS?SJG-9Qo8Sr#xWlr~iy@F`x0Asc{7l zpx=zi9T#5frOS&g_&m?R(O#LkSZBxZUK|>gZ#E83#g}r0ugsi;{+OqI z1`jA#cEl#eW;x^i67!|#8SFWHZqi;B=0EoMdtYLFcUglM`uMIkcQFpO{!w7C^fHYB z?Tyi1jlb|)ysx>G2k$%BZQA&A+UN4S`cZs7o<1!kW-Z1$wtUHZrGs)*R}=F6V#ZH2 zUQSuH`N4qp*NEO)>RG|N^0#QMDxFDJo5%4Bd*($Ym~iRNV~qj+!Gv}P6B>jGUwyOh zZ;P`0_GNzCNPQcvqfhT9ma*vP&_V2q>cuDjh;%TmDMh#Bg5-lf>wh=AsdeKX`e-k0 z`xG(WJ+!yXcSg^%w9nSjhB9Bp7t+2KZWHY$HF{L+K$Gxk=- zdoO)^bAk2RUi$gVcU*EUbMRm7c^!OWGBj%r4aTM_`ch*$*o^7r+!wu)XM3P`vo&PZ zKIq+L4K+M0K8*N>LGMuL)fp)TTDKW`r$KKgw27`g@Z>&dBG%j3wUrNTAAH;c57+sI z5PN3Fi?+rv~?WVo7OY%bcp>nQdj_aS_kZoRPQVnXUQp}Q(p+zmT z_*?}){BXyy#;5oX@?Cvk@W{z2@YX>Gk5-pAS7U?rz){lv`ZDA8=Mky>*~a%dM*U*v zPWlu%Vb@bn%C>3m!;Wf<#sm}i1h7$i<4>G@+8!I;GcFw?n7RI-~^X#9vx7M;o z9zS?P^YM`N+VAKe;jGq>JM!ZLRoQN=&6P)wHBRtj^Wt7fAvS0H zpJs0IP6_W|D|$*f-a@)!_h`eural{lU0gm6+0y*PNhReQbn(oiqR-YJ?ET3l*xuw< z^tQ`Kd1q69G4=acZx>U)4;#H=ONxjoDS9ujx#&HAb6Pp$XanoP64owl_&7^gH`M1_ zPgQcA#`?iejFPu~M~Rs*?c<(lA2CWL=<#vl7b>|rFx4?~Q9 zD77D6^Fg^~en@{Q--+h2T0mt_<7SwU#VmP_258+ZJp z#jKN|)nFz(toWrlw4ux*77m&6ptbz5<@kGwu_at+)jbyBnL>DJKI`B@cxV&+T?nrX z=CiFI+cL?H8RWcJ%IHK6c47be4t4n{Uwb4Z*Svh{>g!Lc{e{fKjP}25&caJ?zuG(j z%+mZ5R{j9&D!}Khm=hPf^1&|k6&U-kLHb5MbU%6hFSPPsVb9eo4-zL_h)v-_`cZo& z6`Q;qEbc(o$p$4%RNfao>-zA!c)qIyoq)dl3+mR@#%cV>&iDKAo2af5V*eii%N3_z z&lp}ry|UrRXTK4jz0R-JyV7T+`${iWyPiX~780}2P0T_sKBsOcW*<3P=&jwv33L;?&`s=uWfg*Hc088ii_*r=!Jj9YCyHBpoU6vT)qEFbc^L+0%&`8i&lD2R2A%R};T19@+XwF@kF^Cao-yf}ps_40R42qrG4 zJmc@2km2uip3U@kUdZz?_+Y$f;C<0h%e%ImbjP=U{?5DHJ{u8CWc7Dqr}o-2q>D)A zD+X{Ua!F}Hp$`O z1;o5tL&7V*2v1Jo9x_0*D+b8cjV}y;L-(d~k1_V4msOj1wkyI*_?_>q#XIcyc=5e- zMdtS>WLJ+(p}okJ!O;9OViNRQF=M9RrEBP33A%<4T`&**!cT18yVx?y{p4&V*NL7V z4=r=SU-)fSH+~S-_KEH)

9|^K=RdB!4Nx2hGS2{}Addg%@~l)5sma+l^mkkgxC+ zt(!IX-+d0>DY7F5_R3+2NI zCaQ)86F0$Ed$8d*L-#J$qc!ALi!uIP&@cbR9<`4(RR!(qWIVd*_edFiU5QRKJP1!K z{zq#i@a7M!k?6C>=_~rW-OVjmL*HsXK2BNc*LCV6+AABqv7eUF-b(t3vF)O-p9Paj zM&+%l;d43pV^o%#6aO<#S>x}hIjXY8-BEm093!j zpudJ>tbde$Xe2tYg}##q{XY7|=zj-XKfb-b`N2);Igsj95B;osHly;8qfVZwo#2+6 zldH!=gDd~~TmSbm>R*vjfAs+Cf0=Ri$`jLm)(w&w*c^$cL~e}NoGc+G&9$56x2(I= zwwJB|JB4GeZlpfbnyP}UZNu=V$%emn>GrG-Kkkf?VquhD*YqFxjg*&ZC-K8>|0%ze ziy?0RHJklbDLnMb!*XH_wZ6*dze(~@xt^uh^RMsaeAgOI@ebxbv4$n%_|Ct%ukRQ0 zoquzmeRO_&-V+pKVdeiK#J+}-A;e{&Cr{wt{f+#9*J%Bf&%fSdtujk%t9<_TeL4DJ z$uz$6Z|?IwQ%*m07W*;=WtY>-xmR7_+(Qlhhg?;Gwz4wU#z5=H5E? zpBmXe!MVq^miB#xdplfQxYD`DwU##A#=VGe0U1?ut#glSE$#Ud_n0g8y`XcCYb|Y? z&ApT1!&dh2N|t?xadpS@X7&qu`O!O`QRb?5JZt#wuL}ky@bC708?Jk3x zUhTmugBAK*bFfhLG!$Fx>#w4o8PrpiLp?{*>-o92o_9R;yi=I!*Gl?zoc7yY8NN;Z zT6yAeYWDU|m8XBI>~UKDN!HN|#)P9~d5IlnUyK#LMP-&%NSEm|?*HH|Z-%G58J=;U z>hjSw;pe_jv>Zz z-pHoMG#2xY6(%b9EdG$b&3g6fJ1AeeuDRb^W%OM$hP{P}XDPp;Zeln##izX%>~}pU z9Ggn{Q|$7cau3bp{mgQ2O_h6S9#|?pnsN`-a<$7X4#y^W%AHKPlQPOhf6FZQdT+U4 zOtRb&Zn-7lSgEJniIiKKQ7-GI%yLV;<+6rNmWxfnrdN6YoN}#k;aCaf7G;#ndNH%y zQQmUV&y(e%%O4Ak3NKq`HTO=*OWa?gJUf%Zu`!g(@3FC5$7GaW(zpBsd9a=FJ2VfS zGg*GAl^Cz_1M6cWn0KQoZ$x?G{w(ubc<(dw+s*^1ajHh&b#*t*?_Jd6%Y>Y{O*>^dLpyjE#7j`S(D|W55L`ZX4q?A zevvWLI(a?*pK~BRramxQEy#Up1P{giNTLz>*{dyAbTK~Q^oL7 z^a$l1%qVw8-*Ug{EqA7;+?kYXWO-pY+Kmsmhw{3E{gvg1s&7pn>v^7fYG-@KdbV2+ z=Sl3Mo;daF%AuY)>GgcdThCliJ#*c9^258)qJr=Iz4J%hs0 zSE=W9>UlMXdKRYFbFR0ZMV@*Vx%Cu;qc2j=E7bF14)rWfujf5t6OwariKm_=ZaqW7 z(e2dJMm^hesAp++m#Z-p^ZQB|?ro zaq;b6DOdI%$&h)gQ}QviT5ZTX+B?a5CWHXU^49A7IDL>T?{05-v@2O2?LF2u+>GHdzde@q*k3|*oC`z$#JQ&!6~&yS z)j|Jc)-l^#2jh~ggE2c+>C{ngU*qG{hV#O4#x~BqIO7&)%+u;%d^78~!CMEsm8=7v zJJvSLX@^~h-H!Zl9A1rcFAmSe;o-D8;L*%FF7(#1-BZVQE769G4b=?|%NK0M6xkhL zO1Voi%5Cdg?g($WFM7&-(JdDm7kSEEOu36P%6+A8xrswb{%)W5s;Asn;qS4O!MqIB z6@^WlANiN!3;At+M!B!|EjR8h_f1c^Z&EHfl1uREhOCRiio1`qzK@5v&dn(It-j^{ zr?=c)o^p5bzIeOt>TrBEygi5Svop$z_bu-kZ+YFG^17Y!tZTyYnUr@c|1;AtwmsC7 zltZnq{;`qr+;vRoU{VgTj^TaU8C~s59ILl^Z9Vo)qjOhrFElJH+h?>YqmCne>-e^} zj(0qDyyMhSPaP$89Z~61b;$EF?)j;sl51H;9mo3CvDjNjucwY)=1Togd#<$E*X7K| z;<{pU4o;MN%3Y$E_Og^t)gD66a&)R>Uwz(N2RfdkQ?=Jf_d||G=hKc#yN;a(AJ9)G zQFaM+Oy)W%qYiY%%sR@wb)df{>p-V9bK2yiJ;bZnEMc68hJ8gy~3`}N;D+o!cZYxV2G?Xy|4%`VSe`?tDl|FbF2 zS^Kxv+?rgowN~HiPnZ~>`m;=Iob2kibz?UJp_6QU2XzZCOdsqZsu&klsR_X@_ z8-#<{9pDT6Kj4j3H`+NX*!EAT{J)@l#mu?3wzTrGiFwaQj);%p;TZEJ&V1=$9-3Ha z=6E;rd-r_eg%(9@gm#$7Y(orblEmB`OG>RD|n`oXQuJYbe<^>{IfGIuD$FMlmDx)Kjr<5$GxW*k1u-01134! z9S1XJ?~OKNHp}yj@u!|*%o;pnrt#9ag;P)Y};^b^JliabmQ^Hy$kX2EecXckh&DpCVxGCYWP-iZr*6Ex$}*$ zPyQY4^)&Ji`DXiihqc`B@pp;s2>x2{WR_>!{)QRfP#pX8+m*hwHF6#@PHW^|zr^gbxB4=H20OW zX1*2ZSZD=07g>R>#a5tui51wr)C%lbW(D?6%4^;?#ov4Y9*{iP%RWBiLnpTlIvDQ- zc`wL&LEa1UUXb^KyjQ_{mAr?&-m#}A&%+%neoN)c*xv|`t%0}TxmX*#wG6pkkKA5q zCAwBgZs!Mf&(9C+S(qQ#yC^@ceW{Ue&i}c=+o_SK55#eMhoc&xM9tQ>Kj!KGH#- zs2`hdu@3L1j0bMA%$dD=7!&=rgWm{VIlJj|*5RA=+ZXJ3aM3e?F>4Yg6gE93dDOIk zTxjH#sMa#dJa6FlaZJH_7!`+xaNou1}xy1;l=u zIX}ml^EbgO%vUq#nXm4gf9SKPp7SpfPmrARn&nC`wZk+Px-4L#Yt{Nv73NovqIDd+*KQYe~gD4q- z9)(=FAUyh)?w*vq=2#o^e2u@kb8UWecgV+l@-d%$%qJi7$rso+Wsr#j{3H7TBZU<_ z&-`9f$@9~Ao_z1~%Xz+>=gWD%oab+B9_rD_g#U-z?ET?)lxTjBO70JjFrTB$=NR)@ z>*S8rj63tai+SI@7M-jKoooa6vPt)CzPX=RvKZsCPWqzqDZ`7e{ugmhE4j}zio=l2 zSFu}l3oblr_7MijzgcPG240K8>x#!x?0ScZ4=6D6=5pdyIv5|VMIy-LIAc-+?^eUR z6~r4zUKc;A`B(NvtPFlfpPGx0G{ju)m|^F&aQACQ_^!F7Iq?JNTmhZGgGTXXoO$Z* zGrWy>f(J(ytl9}SY_#JE8WfvwHGK~c;GZSNafmr*uN!%?yNbT))7~L1 zU(IaqMB-zr6nkQ~o3_3-bmSSU=F{dXr_Eoc&Fr7K`ZeMKE~dR_ESP`vQpRZTg3_bv zpKfAL)@r;q1ruuP?X-OpGEA7DeWXL^d-FSeuyN#lufI65aMkmSvtnCDdivnJlk~xV zIDL?el@-s??r!Q_Nj!vXaVx>SX`FGe0^AekMK>V7O^jb*%U#HCmminI*ZSS?HvO7e zrgAkY7q_4O4?W~3hISb-w08d}K4`4+Dj>_We(&ui=R@x>{)ab5mO)cJ{(Sab7HTx)Y zkzca6y=>x@Qu`J5qR;f88|*`7?*|)=?jYOFCTvEVu^(;0ezesW=zbcVa2q<|c3upgmEb%ER6?5Wrd_U&O`-Cp*p^{~$0XU=0fv*lgzIpQDD zV&%DK@l8+3=^WRZGu`&*u-5z!M<;ddQJLq(?E`oAGe+8X8>`O?$WGsZ4k*0p0UKzo9Wi;!Rr}LW+Y%eftmgm_UnY2lauHc@oi33Be z#1Z=T$Ox|V?~&1>$H)0+Bk9LcEeBbD95VAx@vtLX0_=gbZM=34)PKx7=ixUF-1qt$ z>1&RSfiBjWvO{+@#H9HzBd1o5$FlMYZ_p;XPVXe83wdVe*H#R?-g`W@C+vE2wvq$UP)OwR~ ziZV_y#wpG?bu3}ruq$=Zw(feyZ6)i?RjfC$EA?Pk+J{|fKX#=9*p>3g8*;EdKM-BQ zxv#|O#@ZMM+T4MD+qn|HUd5P&rUj%&$LXWp$iF?vzrD!69{75n`icHJfP6R@B37CC z?k9(6XZ^Im^VlG@_KZ)3wlZj(K|fa!o8{PWI_b|F(c@o0kJo;U_;k zpYllgF~DLNWbs!IqP=%LAEm2r{iryWF<0pHNKKk_@Wa0YbwB}F{pWO96s%a zPkZ3ggTBClBVeH)S;$)A#}UaG>>{fxd5?Bg;!A0o&U-iV9=zHOulB&J2g`Y{ocAgM zvpqa(>?01p-fZ?5r})*yqyx;K{Y7B)STLJD-iut;ykz|ohPV>?(5|{&L^VEZf*Dc~-WE`a)~UbEB}C z(#E1JHq)17A9ZY|*jkLu^b-As&2&Bd{R3ahUMf4S?4`x{n*QE*)0Dfglj^?hI~$&~ zkNyDOX1_pS9@ma{Dgr+sp4|2u*!>~8p7`wX#(Bi{uD~WKTd-o|#s4da$-fZH*WPTM zM{jJP%#S+kLS9}fzgc}U)U2t!zrXEIw>x(4aqN!F`C{dv|1NgN636ZcA6oK#;%oTw zEynKHP{SCg&ui&(AHIgE&fcLK>=DhoJ$A>5@CWUXJxKPl7-MMckBo1uitid@>RhK7 zZTT9Y>-*(NyJJJj*Wk===2R^)4Du6<;koK@vcKIBo(5g6-4XiRlXgeSckPaKyeGTk zpU7LQF&>D2;n%x7Sl&9dRG!Lp$0&mAkD}8& zhTpJyur=jC6Y_t9c*FjEp6uw1Rg|%gjU`^wpO@x~n(Ok%LMt&I{;1ep8rC@8kF8hZ z=<-Kwrp+I<4u5D)!uN(h6bn!be-y(XC4THU@uX7&nJuk6$g?u;n)Un@7-+ky{=2_;Q`|LFK{oKy;gU%v$muGi?^Sv8b zyYP>1x1r*5mm7O+tZc4$Vfl>`fd20I4|Xq7x6u9#P_rv-&3dcVDkJI z+22*4(XHqUji0Umx%*6QUeAS3lCrU%Yn*TYw?{7aV~z8(Z1&Ac+dp%&(U(Uj_2qrY zvi-=k=rZJiKR=+D#*S6UMPy>v8sy>z#Q#H5XAs$tRh^ai^(T^19~m*??Wc+M^0)jqrQzS4Vt-iH?pnF z8kzLVN{=yf4q0d8x4$KLiR1U@9dB9E0mR#J`TN__F?bMi(fIz5e+Gk*v5FHjzQ3}R zjQ!SUPL07Y5o0kP8EgFgmFhPz7&&eH{IpZPz8G!Q+EMgL<|gGQbp5Q%i@~n%?=Ox@ z$5vbKq7S0W?EY9pyJP-oj0<`-`g7-M^zJq2)ElNTF4Gv7X^aax_10;D8_4m}rkEn; zwtR^8oVCl1)5p?@jo;T93(@xTPl8Q`zg>Q|pO55Vo9JNgZGUYOUv%(V_Nr!Mk4WV| zn0h_?Yw)AY{fc$?W%NGt9sfz?@A1$5#`eEeb0&d&N3Z-L!|q6IBfg^s`Ht?c#Xd}1 zrQa^oS-fC(r4@W;-sRu<^>g3(@gJ92`3rsgRxvnuZ)MkmzsARB&c(L-%g%j(@3CFJ z$DJkkqk>n5w?Aj=HChk+i1xmkq2H@LdrW&imYiEr@Km~4Ib%|cE~)$n?!HwM8^eB? ze;LWyBiyfK+(Ybzn)lwWmkO{MJc#~jugl!Maqjp`Gkmhi8uV~CzP0XgR`?-&Q!6d< zzTtZtN4v-$5+1B|{aEI;<`(?0MLajw){`U9BA&I|YnNwa(g@@t?Ymp!say#oQu)8t zzMC`h!9=BVPT?KqQ3rF#ZKuAY$3J;H^=M9VK4OdRyX{xLeBq*evu;1c&wQ^Dyu1cH zd$NMQsIV>%SJD^x^u=xTh4%VKrZO+eTph!aNpr0{*JD_jB#<58L^{c}F#yN;ZuI@_S2(4GtAD6lPAz#v% zVB%+|HiQh3h@dp?vP z3qp05e_->Whq3POeE6~3#{Zf5VEUoRGats&7ZuhOAIN-YRzLKe53CKZ4FAzNqtD0& z&wNOc=|NVhlW$lhasn!4?a)d zc<006%0c%2Y}P(+R}N0wpB*6<-0qi4!*{4(kXL+1CS4c4P2Y)Kf1B8G<(&IdWx8Hd zf-WMxCd%)MiILr{i1(S`*ILnzug_=A z*t^W%>__faA}1ygSSH)mECAY(UjFOIzGEV4|jnfr|1(^;)0@AW<@nWnXF z2Re2qytQs*9yy%syhGn7?lR&pH*!$+3HSZiksT49HSy(@gH3#S#F2Nue9Io+pLpa; z|N8Ta@HrX%IqDSs`6@Av1MSZn{}=l6<^lBQ-)8Ag;k%vxD5a0>t;7~tg{@--y8TSX z6kONYuyO399oR`bv6FUTC+&U*AI&lB{k^`x-{%LXjKF4gkh~?oLI;pta-6dtU$(;n z;W$?}uJ!q;=%4V4a4Kll=h&%AeBmRUhbP@-M~Q4>6P)-sTQ{??Ei3=c!Qsq#?5}a` zuN~N5JF&k;umOAZxDoKa1%LPV`Q+Lxhq%}Y>QCuQIru6!IK1rY53WDz<_v$7a(nJy zM*qyE@9{&$=QFPsGXEB_K3lBy89qz=P~{S!6|1l4hI;Aos7eJ z?>NjP?o#8x)z~L)4JJYw2Xr*~0L?gPFJ#;o4tvL;uEZG!+qa}~I5;<$_?5;1pH_T6 zYm|k|`9-Wz7Hf@yFN-=3F^>;X$DyH=KZ1X3WwO=V<7Ah0rZY~btGry+FHinX@}+-q zk8b?DnJ>oQo|-S3ACX;_y{Du%pEK6%_^Hc&daUtr?PX^@5kqcDzT4j~V6Q*px+@r{ z*SYN2Y-)$)t&$xsq8NhTb8gESiXmWs8u3`N*-W4B#t_(ZfwBVZQ8&KO`_+z;Am7WI z-^}1I(K{p~K7jRALrrN_!k%N$27buip?cn`u^wG+Sx?zxWb4}6D^m_HX)P0_9WmNb zCpsS*8s2GQwM3_FTY86B^&rn`?%hp{mvl_&(*GYc{@z1dlKrDKK%XMP4iI`|^c-;F^2ypLB1Fb8kmM+Pz0~Z?kjHIwS4gD&5=U z+zXzWc5emu?7Ge}zrTe(R&xX$+N@WDiFhyb2mUoSQ2MS7nH8!#Km6~XwQyaY-|W)% zH*PsQ&b7-wn4){3d(Rqc-#cQN@@wkp>)Fodm3&@HoHBa=EZJg1h2iF@$Vuuamq#MD zmb|X28ylxh3;c&H_%3v~H2kI7xxsB`QCd4c<#hoPm3&uh|Y33+K*lZ6)K(qDOK z8S14acx4(b`7SN&?abOQ>ZchFEt~kfI-QoFmlpc1<@JCMJ1k*?qHTjCNd`MoW}? zh8L~tv(vJf`l}pTw(xmXIxXjWX<IYBE7;WeC%5+-ZD*1EJ<_T245%j?t+z8PBH zd#?BCmmcb$;?Q!C&*&Ro`4aZhveeMho+e+W1QT^O zE$vp_;BX*COM7w9mLv3wxp$RK3o_fD>lw7P*R)apWJ61P&5L|qVrb!ATh`s>rDaJL zS|$e*U-Zy&u9ub__8HDDEhAi7$Z?QmjM{5nq5eq@EwA!FETwCXnPfM6ljm(D6+Cz00geSSrmN?IrI?s|bDeYOtaf18oMCV!RGUbw2 zEA3hK&7bE!%lc3>-Nu!iVvL(B3MEy%;9eqm_&JC_!Gaann42KA#e8CquYd44)A z|K_D-eimBbjg1~!zT~B4$MCd%neEbI*|Momzf@8`I-H@UiqD~RTGn`J31y+Bh`g&F zTBf@71#OvU${p(P9JY%rIud+;e)t|w8JBw7QGYDhOth2p62u1 z^!Cj4wr6gZ_KXcCKI5UK&`S$Bs5G7~|649C!M<_~*_o7A6{-_`_*v$71v*(=ws_|K z$Nmc5Zn6?l(cH~-3)dbGy(NX=$>!Z<>F+k-1M$AQ%Xv5Mc{e^NJVEcWXCkd$-u?4m zdg^`CdG{^PyBh|D&oT9Wv(LK+z3;y2y!*Q6-O!-$Al_ZiIq#YEM!fI7=)C)i=iQP) z;djX28^xEO`R;4H3oo^X$U_k2TvT(vZd~{k@9+L^B!7S2^LsIVBKP_2p5Ns^a({ou z^Lvfw_pSVH#%Oaep zJNf;hg-+jDi+6`y^Y2A+3b48C+oS(xnHfgAnU^L4bZmJEtfeG7k@*~JUutZ zo{P2qpph}5LhLZ$o#w+}2a{@Pt^NFL(Q@vMP z@7!ORx-b4aet0Zm-*2rwR;Bx`A@T|)@55`qao=C- zyic3m`{KEF_ddL1%GdmM$`_9bPYnIrocr5T_oM7B+u`1CbMC*Gx-WkFseAtw=l-jy z`{Joq_dfE))GvAHwr>Py`MLLpI`>C-?)&e1Vx4<`v~zz<>V7%n{ayF|>(2c*Q}>Ix z-{9VV%elWRb-#xD54rc_&i(GxeII8h)VcS2ocjk;_wm_0@we{%BhLMIJom9*+#;d<-=qocmb6MN-Ib&^aaBG|7u%p-B=axZUJmG{XW1&;V zq7#?VLK(*1-BxJq-F3!ScvW}-<<+ov#D_h8z2XJGFvuDsoX^M3Bc z+ZEcrK_6%I$QQENlas~7pSI=O_LV8JuMA4Fuk6RaaDbdQ2hr0GAk+6F-+PW>_w5Dq zgfafYKn$FUgHas^u{|Ea_PC)i(7mZJuzPc1V9%Dqz}~Heftj3D>L)HF?(;v4&%nfm zY{BjrAwHUTkXFTm*ypSb$NqaJzQz$P(}^GMIYvB4FSbhf=^*_DJ=~{_uR7z)IG3>x zx0;v$*_Y13c4@|kaku+nc+0kNPD}{zWR_=q$Izg67Kh49+Ox+}sz#HUh-pPTc-IET#MCqOJ8blPXj**r4H_K9Tq zP4Qs#*DS@@?7-%>&W^t^{T-t}%)U_i*6MXD}o2KW9Up4V(jduLm?c~2n#-FV<@n?su9UlqT$VRwWr~8e68PoVVloX>pO~?Q&vvUc_FdeZhyw$Js>xBO{|5Ow3NJRW~C1z3E^naz%4b zc0pl`$>Rs+Dj%Z3Z`vaKR(=fSFkGkn8(LRV=WjT#PyB`*6y1$@91l$q^ppL_tJK6bFCU%C6H=^r=2x&)@~q0&-n?${)EeFo1ryp+FB`D(E|ij^shhY6 z&7b+~v){=X1Uh%Do3T^;W7lkC!maoqR?@G?%3}Nhk=6E`F#Ct<8IQ$QB8Dy?o0Rtd zkCn|7T-8;$s(t!ukIqA?K7;)TGw~M=v7TZMwcgDf>agci>phd8*IyOr0xPaT4;6o}WjKh85^m zzkOb@M^5bI4DK(1ci2~d``MiHS`|#Zh)mx>u7-`fm%ffKwqZWL*!i3Zp))QwVHdxi z*baRz^;?Mr>=(b7b`a~Hc!|Efn06@VQZl~F!>8JVoyNDKjW($bS3g1gqsiw;Jl<01 zUPc?2+VPLKpCb20GXC*(o4;gNHhDK*4JNMmxF>eZOACEcL4ArNf8Y})j-0u6L->hl zsW@`x^PyxM`NCi_j=b8$!e0;$Z9CSu#~S?bCG5w)q}U4oV!<6pU*^obxxRc8+tbZh zok47m_OECRmS_&Iv3YnjeKetf&(JFV8lgU;AET94K%Y$#rui$a~XxZ#wTG z(}}$_cm`aTavWBtgWZra@c-+KTueRr%Fsy8xbTO_3iwD|GvlGSW^koFi7Qj^ zQ^G^m=PnLMM-zWppNeb#9Q!&3LWk*tt5a>GHZeRA>aVXWm!ZU&VBP_5xI(1H{UVUVywA5&lQ^FUF9~#M&hyGbxMDv0J&$=6_Pq;F8%B zF_-T}d=FVRzcRLp+f*BB8DIHh&1da}fSx+n|JLRUzkD@vXCm+NejECEaxVnsd&fT) z`6Jmg;`Q8QPuvspZRAhA9q;|}nd$psdL(-qkwI(G^_#fTUy^qRrqV}C=<_yY(K2Ka zGN@xEyn&4B3L%TwE4iD!l6%-IxtG0?iwfE6imrdiXUBj?nYV}7<0bv5XpnuD&uTm7 zpg}pgq~EAs)i3=!ssE$D^f2!G7Ef6{Z13v5AAWSO&(+a?} z0x+!rOe+XvuD^s)yWv@3*E(Ss*cB0m@q3itqnp7yVb>NL?`n|I!n|#{$WG}hmHB2p zWy%xoIptMTp73mwQ(nKYEVDe}q*GqR+5h_h`w)G>+b$RfX09g=V=3_-HJdAs>pOcC zXZftZ{pW|Nv$yw5j>EeBpOxwExVu5HJ8Fy!n+mchQ(*G4gTah>U+1^cvtIS zQpmxidC03yGe7doSyyfho}E7p4EE4HR{HpwX-3Wo+w^^2qy1g`o?Q9oeMi6L+CRej zpo8_**|2^|7Obc3)oT0TOTz2T8Zy=PFFI|{l@2rR!g>!MX|7%Je)vfE?v%G2KCn<;q+PrDv`nNI>PD;mij6P=qy zoxX9waIClK*E;8M0sWo0>Ro3Z7n%J=CdLk#f4XT4pK#m$JG<@p`^n9XjuDw+~OyA>sJ8^GDXIRX>G3{S%KQkPg z!L!)wl@~UGtVc|Q_pmQDhK*V}t8(LMkJ;n+G;{`}_PXX_gNflI$JYyA3ohe~mI0|&~`m!{HJU|oOpZLhBF13#r(TkNqsnQnd8m8VCyuH!86Huhti zytm9_>G)0H%T3r6n!p=$uvyAetK5$IeLeJD$o?Yjk2d*jk&ob^e!Ew{DZfb*vcvi9 z8U2>8-lH(Hm#uN?6$97TSN=9lUvDU8?nz&luT1*7JNM4eo^a-#kGU6R z?iv3J^ULHoVD9;td-6~EH1}rXo8ewkpGT$}eO`LK^m*uZ^m%(;q0cK{we*1>k)wZ?d)!cYpYk?bC3t0Sl z`Tdeh()rz+AHl5ka^Uf7Yk`Nf-<`F9C0&uVKoCsTT3|W9%id?!0*$r~v=f~R{rwti zPK87dT~WU7+&rcFOCQEb^?^46Rxh zIOXjaYCYL+ndT`+`t!K#`g5zXmBD+Cj+e>-045NpeaO1((r~l@yeLWO&)>Pl=vw`g ztsXi|d!#=*?WvKzYt{bq#qX!Tr9Y>acY&ij4{ZFTJ3HkC&$7oaGS)gCUF+!1O^)t- zwQa9Zz9MXx*MMusjn9y)FIam)@s9EKx((j5_-1;jGWVJ^J)?p8HYqEK_=vrhjw5lCiOYYn3Vl->X5-SlYMLwM_J=jC5a(6`O z(~ISk0_UXj8C?iFnfH9UlHa7+?^-Kv8fNG6Aof7^J0p+l&I~Rtx&xoP=yv8JRi{ z<$L>9@8>%AbJase{wg=w{5_BDFH=76A+P)gt&c>J&Hd3wJ_UxK4qIO}w!RJ6`YN4# z%gdO*%h4~`1MzdsRp}DTm;*j^34S~7)g{ISt1s9$ap1bdSKmz2C0dK&U876DyGdOF z-hGF?v+LnqpL7X$S9osQ0MR+%S7QV0htBYE@#P70hB&+(bo7R|sIMlaH?$Yu;^c;h ze>3%lZ$CM3z2T1k%#AmtH+;d-8^oLXv+aqFeflHf&y;^mJ~8>mEEoTn=4PlZ znAq;*Ys&PsnYsUBFlm3j;v;sxru|_30dT^#OCObA)2w48Px|^Fju2zx`X9*G)KGEl za3jASmmR~EU*z-pI%8t$geF59bxW3wC7+j@ujwgrlb86l2bZ>@lNuidI;mM3PC_0@ zw#mPcO}0U!DPOV;3^JcH*K^3WbyvVcDkD?2QNGtlVSH5c=!exdE*f8=?60!JqIc9` zw=BEbx@RYI%{XX%<;x;*c&BR zb=CS#*R1P1gEt&RyWzZnVxYbf)<=)1RrpKQ^u9SmXU*c?D_nQQt{+TsJF**j`&R9d4yvo{sk&y*^<=j#SInsAyoX33_}fokb&+RI zrs?zl1MAoI@R92aa^u5%>=DiuANKRhQkZd|jQB9*Su#Eh`Kf-qoB8F<8Kkp<^n0zD z>$2AkZ<##Ci4Wr)7lYjLyfI-WcMxkeufFmQb2QE0!+e*_U(bAZWIpS$QQ^O1udEqg z=yB_p{Aa$m*AVAXLu}bI#1xEYz8hU;26J0J$(dZG&p=x&#Fcg$dk$@tJqJAn|Bsz} z?O!gp@nD_yMJraxqkpN+JE=Re#IFA?+TUMa+FA!wr-P~6tKGGEFmX}7eWuJr#zC<{ z#s(_f5{=O3`VvZVz|>rL+x1<&@=9*}E!_E3w)JZ9$@QUJY%=Qa&?_FiX#%&_+Wu9o zeGL!1%6K^b&PUF5@MbM|)0F!C3)pEQ$nPk6d2DRo{D8AP{AX;3-~PDC4>(?U16H6f z8~*zpd;3hC&}3+%ZU=ALUA#FD{!8LbCjSW|#DAq+=ZhAu@SiDDx->MJ&%zsMHJ^ny z(Cp>WeDSEt;Qb9|FYF2O1A@CIcBRpSH{=7nNo)SWOdgEliehWGO!2;pH&5jJp zl=(KMWyu|=zPKgTKJqx9ZG4|z?7cP_7h~fr`0uRsQ#i{S$N0*uF=3x@Hk88IM?Re^ z&U$Pi@-;bqulQTD{`ah}V+Q{uYi&F<*sC^qY$%z}RDiv={m8{$#tU65QUc!DSn2jzf0rwV%&LKFMzUgJ0w(n|^%DJ%RQQ`ZAFM}sDcsz^dT$f+ ze;GLFCw62du_LR99a)V}8Z7PFKnxOjD0h>Gau0bZ_mYQFV;rjY;4km19F?bX4G*kj zJRJOe;%op$cL)B?AXl&O7v0Up-*?&nX6l3{ zLmPEx;%@-{OX9C|KIwn5W6j}zQqbV7S??|4J903#*lCCG7aC3Z&U(*$PObNB9TD0m zh)0FxyuZPVzm#w5i}v|R#ty3W9=eG3F_`rpcx&P^Soc{D_LSr2G0#fgJI@N^gmrE{ z+eu(Ta{b5L?1M{HCSJ3_y3ff&i;k_hY3&2pN36Bl(*O0JhkoJ9320KxnW0;HgY%4? zLtpr7`|%aa9K^TPTTfmi{yVX$bWEl1%ZRrzaem~%VXgY=rSCT8@x6k)II$AT*icX3 z8qsdgK>8q;xh$+oyp-EK9)%8_wmv-T{IWQ^OD(?wU+@8DCbyFUE*YXib3 z{E7DZ@Iv_1!Kd~t>%;atsoTM)_PeORzxcEod}?EFQMUNVICz-cH{@9xUUcCJFsTq( zn`12gj2!t^`14oZ^-3{HgRoO*>Nf)9%JZo8MFTWAh-iuec-)f7mbStpAeuPeA@AVOWLEUuW-A`7qxq#Q+DDqw1H}gv%8>h%9$;=QEs# z>$c7Gmu;78KV=+HrVY|#U#w$Iv(fBZ_;|PzoG4(A&qnr8eNb`sqT9jEcE@hoZm%(& zJxJIgQ~BEmW?THqi@9OO1dnYoY45Lh=HG`5KQ@;313#EgS@8p&boZZ*J!|0jv5PtB z;)jX(V16SX?cC((P44`zo#Nn!Grw!8KNCOJ6W1|-^-U5#{^8j|{IeqxCjdMwIv5)t`4`=;y^F^n1{eo}bgCBj@FYUG1TwMIPe^mDM%co^~1qY(i zTUo=row0tYa`}t3gs~s4D>Lht+SK~R;Kwi6Luce6G#PnF-I@3?1pZ3mN9Ovaj5WtX zXg~*uY5g*htG#|{SFEM5%9OA53$&Wg!VhT1erWR`w7*>n52_5_XZ@1G56bthUwX%q zuXd{SOxAS_II^2~XxS0hIeXxud^gW})(q>;vYr$j{g#*#6kUm}o%L0GQZTWMy=M8~#R~AE zX5@;;B5gX~*y^bp&N$i_R;>+h^zIRd zc7xG%XNHS~(H`1wb7&vX_;}W<)#Q~Q@HpR;8s}4uD{XyhoblI;3A5ik{6%alTtnAeXRS)-hC{fIowivLD7+v~@% zBi{%96Yt}}|Au1Zv*xtoIUM_@kf6+&I%(p4zuBOj|#ma8GUKI?#<*F|ooEz#YY_fIrbJ>qyxy zCxWH+S}nE599+uWW8PqW7g_FKnzqM$J7-^ten;+_z3y&%;*LGU-Xop9*WJ)Ojxj}d z)|yw(y19JF@7P97<(l1Jp)h7 zeE-#Cd%o8=^IfvW!IK8p9yy%2qFnJLIhWDne45L)zpOQjJ+k#q>Q3Sb_4gM~6t9x2 zJu+|H>BE!K9C`OdcygNUk*%Q(r@)gKeF16q$ab$ia-2Dj@nhlW z&~K(;%B7qWYA}Sgvgt3|9@%;t?2)Y-t;xn7*?EK3VIQbHGG|Q86*q2rHaFb(klG`& z;>Ysw{lE|AQ&#*ibKAy`cZLldKejO^J^0aD%={L9M48{tdc@4{8`vi*KCN-)cP;g2 z;>S0Ljd?%#aZkx9@PoRO_(A>s#gBp5Bmd@t(}y38oQd(i)+3HRa>(h8r)Ujr_Ta~+ zKJgT-wOdm7@g;c7__G)TZ#>1t7iHiFHa)LDtHk9mY=huOd&Nt{ei2U*)Bc~Cmh9bz zzkbMmNh1%T$;d_re^GsvO_MS{35QlJ+lAn5om88e=7X&*aYRz zLXYwKvs^oviJ!mBiJwRBDgJco*{i&35$2j>BTKDCu#tfq?bde-!|Tb->$Q=6uOipA zh=*Q}jm*yNCi!8<@VPM_$_bchH$Bm_6NsgVVCRzGO>4|J>r3omhb+!%Q5?lL=u7Nj z#8GfwnX`WR0OBYb99yN?BM0x8J#y)1HrsuWEA}++$Zg)9^C5|!V6Av4+j^xFo%j^? zuZFvse_8OR!IlL!-pm;~aJ(r3Bhv5&d0_Cy*;BRSeE0|Z#N+I_)L7Qo^1#$Dyg7zW zU ztXKSY{-f6uw*~pVlKEdr4Bu+z{~F?{uxoX$C5A6f3|}`f zd_BbQ9VCYDh>78A2yOFV4)3cRm8Wve+}%!}J24bH&u}n@aX8??qE)QBlY34}@qyK+ zW6p!%gyFHJ$R*jp9wLSUyf85ov&qTn@YoQC$7-QTb%&^1m?Iu@ZD2RSV@b@JLwSnf zo69xC|CFG?7_%RB3ExZkPMxM5@_`{2&1d<*kdx-KY+%sLy4mJKXrC-T#8062H;^wN z8ACz&CI^Q<-{h}K`oO^8Vqp(DkQaL*jt{0zF%?&WJ&NVivmZ-6+ff)ybmFV(z*j{M zAg+~M`979$!&jxfUD(N5jsFImcCe{6xwmVk6WfQnQn~5pMnOP8XP;B$=%_$ zIi6`JQ|=C@U4}+slhd9U&njoDi%llJT;%s|uDHHsh0XGpMW(`&V3GDD#HZqi ztAsaKSB8~{FDG4}M~)2;vUKJm%%&)jjI;xkUqeqxPf z=yeY^fnyo*eGRp5(%%j?eH0$c7T?#wn*L<=v|aFo;VdwTb^~AX(u?ZgSr+=owz|F}hdt%`hxwKjU(DRM@n!7bf#b_A=4=|iF#ogROD+2<4Zb+@ zU$Q3?U)B?+@qX~-A1*jCzN}@B#+}qHe9`B-sK3AX(l`Ko`N#84AHHn+S#G@BXZ_>g z%aGHEFY3ck{KWX8`?cNlw}UVD7iPzoPb*FWUG;?if0w^(d}*jS`zqtF8Sli8gD>sI z|IeORQztYT+Nj&Xmv(pUGX(z16<n>yV~{TuqON9d^u`^)@`~fwp`a*#w(t=ZhIe? z@?V_ukPD`q|KVA$v}d!YjlyPgGCWZq-p%~V#1qrEX0F?KvU1SC@ni;b)PpAt#msl% ziMwCco$tS5pP=fkapt>Zje{r2d=htxTX^z~kM;vkm|t1(#LRUYPmbgd98aERj;7%W^F14$e3HGV22Y&%UQ7L%cyceX z8}A2C?i_mxJfZF+o>2b>i6?iRd;0KX%`>_2Z67>w;vwz)3TGWXUSD)nW0skJI+y%L zMrOa|!ILKJUu*HrUo{p5Uu6_AmSsCcoB{pzWiX z%ymA$6>(kYw8QbunDQOpjQO0oK7saMeiR;58N8piKB0VXJ}#3ZN9&8mbf1i+yhhp= zqxH$Bt$Td($*jSbF(LJA1RGHlpU@|?4$wVMImvYjJ`JC}-!-*P!N*ap__CimdG=UO zxpuKfIOEZ@X+Y}~Pc9&>Q=B%%9KVcf|2kRTANLvFdCG0y4#fso7ZG3W3qL}?KC9RO zzMoeAi&>x8^wa)!`CoL^J|tfB zmU@&wf57#m;k?ZcvO(H8VDehN{wQNeu8RuJJ^vu_I#+(k6|Q3S}_4gOQ%Ld@z`tP$&AAb9Dk-b6B^4< zr3b%RH+thIbicNW{&w*D7glzA~4;5V|`yMA0v%#-1-kd=5=@#KR?*!z)m zwia?8d$+;wvmE}ag(gEAb!X!DU&3D{{(k0T9}5Okr)h`QkNDusXRRM?|5m&FCCd3w z?dG?D_Upuh6M2{S)9`zeyMDC&TfyzuMN@SOeZvj#vESm~gzrGI0~}63%laH~Tztvp+z->K|J7KC!}D`banW zP?Kfd^PKg-V>$yuxd|1+-p#*_S%u^*%x@Vv^f+g<_dT;%W7mH?w%g@>=j7{2`}th4 zY4}sQVbkyrkAJKo+j{cmb54mzt#>m&v*1yyEh}t1+TNRJ^jpDS>uirqd^=S>_j=~I zi$^=zzgUdC5FWYbfgvv%w4NNqykmS>I~sXm>UZ#{!HqYcPE1U$coa1IA?)>JfSlkC z9x>O`>^^6ajkNCh5xJRb$l+f$ z%(|zTHB;o)U}7ET@L@YKydHZom@u;J6=b{CHGKaLYjjuE6>AN}9)87VYiw)%<%uHB z30+Tq0`tAFJTU~mM#rAwmL1Vqah#tWr7RN@;3@m-l-=7qvN=dzMk}APR|+G{Il=8# zhvQc&$qy&(f1FWiV#n(Tnb`3%+Ei20zjn;&M?1>F9k(5C{yEXOgR`^%-a{kv0^vh* z8Jl=sSZy%BRa_rFG@IWxRe1zm> zTE=hnPVzn-M-7PoHcomuKmjyZf*kU6%l-1@=|wm;m%Z}T$x^zBf7y8cgW zpTYB|tps@9UObrfh`tM%;f6I5*#^&V(cgNY7&W|{Q80e$i9+}p-;GlPjMXlF+o?WwlHoyZgN?Sv&C)!xgF_nLVo zyIxwIg_N6P?%z*8k-IK&&^enoX`iX|8-3#D0Qy8_L7G0X|8?|-H_;;|b44EQUyOe7 zDn8Cv;1lsgeL)}wFU0Aa4*IE+KI>c#ZLX z%RUcjre*tUZ{<3hf9NrOTD2G6H+$-+GdU-r>ke`%_nDKG1xCN=qr)Nl)+2XSPNjQx z;ODHv#r*cTXWXLHtMy}qtNQw5^zmZsXlXLnxu>$ef?g4$4=Xf|^kY)yI&@^#x12nb zwbo77sO-;E&;8c5=JOTIL#oW9F!N4}Yt{+2rWavk$hv_+s- zmA#s$armSI9;si|+c*I~N_-;Emh$WrWKEGTkUXn$5_&NG^eyHnF%?gCUdR4RYf|`! z@9lc2aAfeAm*5rT?AZ~oe&O(2X1~4295eIv6l3|R6O83sp8eI|I!$9)+;=Ro$Fe47 z+`pO{OK_Vtv2fOD8~&ZAGTXP>(?0CWhm1V(@aj4+RO|V(?DjP{vbUiY*(-VEZQoy> ztbORvhF8Hl!>f$ZiFkF7Lz9PBlkL0iB<(90CM>|lQB)p?O@RkW;g^ZR0Y{H>`eBbX z_~A?T-}m|@#a8$i3+_1jvNdE?k#$DTIM!g@;P4r}y-$I&=s5DP6|`(oo#-U3=mO&k zf{C9Poq&3&Xz;nAcG0Qvd8dbysCvl&RCq8gb`pSv= z<4aC|*qAchV9Fk?fxT-FM;=NxwhY6zhkTPRacve%*~r{-G^0^m&d{s>08hJmb#i`GIrC#iYJODrqrFYu_RfSKq;s8GuTEmrnv=988wUTxr5zn3 zNqd&L{qcU#ez%wQdWZIvT%|8&VldcvA`BjUlChd|lJ@1h7!3Z#N^A_44(Od@FIj~T z4+eh+gTKuS799PNH5mLIV(|9`@Rz&_9{kO3c~$j+zyAvES_Q$xmJ{G_Cpmrxiof^& zBzOERcJa5~KA+=`^V8$E6z_Wgz5F2a_yF^G|5Vn-jduKRF0owstYbQS`42a-j;X;G z;-35K%EMtsmmQAGI};z>h?ecFPtyE4jI;M#C(ZYHL(B0K<@Z!$)02GBxxf0oc0_r? zJ(uGXi4*vVD9`IBsvmAm=>SJNke%xPrdzDTyD9g9o2=Ki=UazgMz7Lu$kJJBsHX|} zeu;kjf^~R3zdZ-eHj%%-qp;~QojcaF0304=&03UiC76!Zh zVC1sX2jA4X(%^t}IGrgie`X#yqwm6p{FZXorfGQ0{D?|FrF}1)0Y32EoO8@~w{PY9 z+$`+yJgfe@3ms%0d(>Cd6^4uG*Y(6yjYBqHm>*1x$0j5@W_%6$Xd}AO1HFy$66_(Y zTfVpISYrV`vyIl2u=Eh+|LOz-3$RoD5Ifb2thIOJqkRE8)h=vQgM2HEf3^!cI{7yk z=Fn`PCoCKq)FS%+L2Y0TMwyG=*y_v}zQ0Fi7}>edlKg2c{_a@Yr{~0oy1K zfOlM8$;66Vh^0tk1#;KOPRUToPiTsTxQgESTx@zcQsHM zaqqw6E+dX2BP`FKozLIN2Zg+N8(&?Te{L%>;%Q{XHe|+juB*5rzYo%15oAZSjoeVk zl=uetXA``#S^l{K{5%Etc?$6J6yWD62-FPMT4U(UU?t1=Nbealu zn)s?t;-=%IS{>Pcvu~;u?u6MRg3v@LF z6WtqviQSumi9MTxiM^refu8x(1N#rk$9xQ^hu zgzIQ%m>a}T#(b%#KcW2~GUq$5^fvnO*LB{C?S!^<%|>^&3VK44>FBBI+vrN(U8Qm7 z%5RcGG30|})&%Lab>qUvzJJWfLFunuv?=27fq3Ny`30+19QwxVE2^#6=3DuzV$3Jj z{O#J)KK@U=S635bG0smu`~~xlj)#8MPl=dh9<;9qr#tAs_)O}i|I3N9{PB;8=cIq) zi-L*UuCX4q<+2-}s#w;%w6QlavCw7YH1CKP(s)B{qWv-2o{UQ|z6G0iHd%un?%sFb z>)qq5@IwpcA6;n`t}6278C^qtHW*#QjmdlpnPU2wzFr9JaoQms)&2R>IcQ%r$94Tz zew@3^c*i3%jJ#;GjQ#!P5z;TtN$pQsimcGQX0E}50gZ7SK2@K0!q;6J{G9vh=iFC6 z=f3&_d$;-nHRq`hZMpE=P;6*b8XMl9%D7Fj5=*b=yyqi9bFFzNn79u6-sn7VggFvr z98HdD@uI(y`sj1`wS#fdx~OYICG}NOUnTWbQeS1DX9{atc>BRT>(LUO^D~M1Dk*1j zdR_CV%jN|en?KKb&Wt%@Z~SzOaTLB0cI+A~dQ#uxmC!H=8YTx5)4=@`z4uz?d(444 z@;^)0)r+O1jI|Bt)3W0-d8y) zuZMDVzxHYIqpiFC)BjD_d*FEIfamuRypTMj3;ub#E#0T`C1jA%U%-Kwa;kri{HSyI zOk918xqGxN#(t{6sWM#pKjb9Cf|hcd3_j4pUKBzqS25u=yF+x9xi zmR-@!;L$w#XNxCh5&w5^F!B2MW7cUe!gZX{fp4RiIo#Vjq`7yt#t6QxpT-z5Ki4uh znwT5lPgluw#%MZYG@UV;&KQA1)Ul89yNG!*a$qLyVy+7hl~)(PbA$KnDPjs5lDP|! z9YOi>cN3pcfG;2YO}HYSe}Qv3)qcgF>#7_WZhzSG$A?^X&Q~-xYEPS|rP{215iZ#{ zkjkw)RM@8ek+0tApA$W6^6Jt*Mt(VMtCsF){r}Xx4SZDPnfHHYCNCf$ShT31Ap}K> zU2IX~lx;J~Akn2QcBOT@w9Oj`mUgM7t@WYX1Oug&wlKxrbc@@75oy9&sNk}e-3^#l zYVE(G?AG12TjwnaAwZyDWdzOh{oUuBnKMHI0qySd=kp0?=FBN&0}IiJ>}Q9%-w)5oZ`GdP-!}9;e|W}itpWDEE0Wss=>P8d zlIU;y28wqaMXYI-9$Om9(qjd~FR+($;~fob4gC<_jB0CqZ4f<{_Ply*LjGECnD~}4 zZ}xqt_02l##Y*TwmTm-XiG{%>Xp8)|E^YZG`IlyG9oizg=IBQI+<3>~+s{KcVr-6X zWYZ1C^(J+KrOQ-9Gjih{)%WM=Tk($i{8#ip)Og4G3y61os{on|zMB{YkB)XexT}}} z;cv-#;%{!eV}IR^z4xMTzcu=bv}|+;y59>AFnlB655`%m1T;Xg8It$ATKvc=eq}6dKLFZgv_yNm$*p>_IwoL!x1y29V-Fi##7UeNd zf-a})Es2J~2d!HtMT%ALye^Z+T+Mlx1IpZqMRW*xV%j$2vB?OI3Vj?Ks&TaY`pB6n=h4@Tj4W-m^)ueH;N zU%bwG7x1wWizb$Cibp-E^W^BtsMz)R1E`>hW85oA{H@xitzL*~LgI?@thsuP%;!lO- z0bE2p)Tp+@b@)Vl`1FZWjG~Li>O+q16?*asY85EPD!B^SNG4xRKWiBCI&1;PR#S|R zIxq4(b*S_BzM`QxqPVQ+65zG?+%_-Q-X-0>dp3Qeze$&@vV6AA3+`Jw}QRX^NgJE966@Z zLNhj=FX#CRo)7Un@o>E%o)7VSi05avX4RhzJNoBH(b#cm8ruUN9t6g{!20k)^aIxN z@Kmngdvp%tYJ~2t@&^;E{lVlKf3S0%KiJg--CYmeZT1ICMrpnm!9zwMM~yT-GXHSv zDcJ@6#BCT|aSHg#dP>Jju$N?8p$4@%?-ifiHu>b1k7@ovEq|TYV1L3M(#QMaEq%Px zhb$*rGYxuu^wXi>CGQG-Z@T}1C&&1geeXmmF<*Y`zAyMAMxHwgZ2R~-!QV0Z8Oz>^ zH&z4_t15!Y)fK_cH5I|GbInr&P@fELh_v$+U&l6=Cf&fU1fmksg{O^Rx{cyY8|-~EZ}4%jaa4NRxGvqsHJLWp2d0h6!P~gS)5f@T8`o#r z=y2NDJ9rz{c-kmPxAERg8_zp!yfAnhmwMWGja)x_UEY^zqtR(&)!=OeJZ&V?ZQPt` zW3|)9n!(%n{iF`%85%(SuIX`^}YHe#MOo=Lay!Au)1P8*vCZ)2aQji1oQCQH5| zU#5*MP8(YXZ=>DQ#)fno`I$C$Ic>BL-o}qTZLCSRF+9`8ey5F>2XEu=J#BnF-NvX) z8wsb4?!nvmwx^BHr`xzB(?+k;M&ICVEc3MSsdO9I+uXg6f6MF_e4&HwxzBpqn4NCJ zLqE1VcQSCJhQ)g&wFDAdoS?yGy1#F^PYc@_cmG$FXhg!&hy?0a{O$Z&zWE0ne+QN?}<0m z-Id`Du@TOEpzR+S==Z>1+vDc<+Xnc(7@vh1AHRQafZyfQaDTsffZuC8zrUB?U7qu= z>3P|7+FiGe?(?4YF7mv$ZIJhrf0+x%OFZvAntsow>280;_)T-)3%K(LXW+Oo{oECi zQ^gLPD>J{x+~2J$Bgfs}XM29fUSjuiWu%wi+wfhb$AFG%!*F~W$KyOxgzekt4c0Z0 zFEW1jdEMXR_ScYk=ihn9h3`G-`Fi-zUCy4od+0 z_&y#v9N9nCfqcIg`Tlw2`xlV!_aooGjC}tJ^8Ks+;CGO@A9Zp>tB3>f5%V$Wico5~ zFBFvApOJ0<%*^pbAiFlfqOFeYZMO02uPuIz+_e(s&DzJ1HL<;68yg+zy-aqo(37&a z$#$#H@@K`+7h>Z>(vMQHBCeHO$8n{-kZKB8x0>2%Q>YE@*wv5+FN@f4x5so|vNf;~ zoR>IoMh6UH`#HC4&DehE*ko;ctg{|t&b}T)SpygT+A~$dtYgdZ6##$y9@}1;WcgR? zexg3~WEJ@?irJZTv-FNL*Je_1;QQf`){FJDyNGtHMslT%2Yn5}Vd&viW9X0XjlPBl zbUwFit$co3pG%y6gs0VM9mj@w)#cW)J+!B>X{_;%ir|Aj>lSy+Z)dz-9L~kJUl9Xl zY`x&eM#YhQmE3ml&E&Sb_-0)inIn9IPYT~+@ICEC@i7x?0$=it)P1gFY<$#j=fcN{ zrk0OY!bWS1aC&&uXB%Wsm`sI^pHqrH~U z(|1LFOJ1UQu31ZXsMgY^A@vy=BH9t=UC|N`4e_qoyWin8`^*Kb+21(&`LPbiUSsHo zCtd(M+ac_dhXpra&Al27-wZfnaiUAlNxJ5bPR948eF}2#Ny1 z&*xb?o*>>XioIr!lecf%O-I0QMqmPC_=RIAe=L zE8@&kcHlU3mG3l-7mRByb;8fw%XX}Q3p35*Go07a-{C*m&)GN0iHh+aXU8Kz2Pa{B zC>|DiI?nGgevfrjoQ#)-o=yPgx(|bsh4ekvdO8ZsGqlw0Bd;xfm3*6*MUGKpR(tM3 zc-l`}&Yy;S+QmmlzUVRWD;fD>Ah|;FhUAW+@V24gJw;f`A4#WS`_{e_N z`ppyIy?!DYZ)< zBQA3V^<)*#S_6$v0LMCfV3WbeNyNxRfvMtEGC1e9lfk*UZv*G9x&WM;@9?cG|8|Gt z-_AYnc@1=M9rO@5iZ1ppfgY{~|5ky2^`eKwxfJF>5A&dhdC;veHN_(%V_`1coH`~$ZP{)zTM_u`B#&i+*{mx-BSuJU1L@XsIl z@PPQ|oT=>a5YfXc!M~rGxU8G(cxv*p6ps}J*NeeD@$d@xc7pntWs%cOKJv#6ZJVaw z=$HFXdhVck9tN`g+toIi-~{4H%7bcVR&v94}k_69ugj84_AV-j!Y9fu{XcMo4juBzc7jK}O@`p@0N3yHT# z^N?NaSIIQ(Ts!zcEuHqr|ncl;UYaY0e#Y5yH4-}fZ0bU+*J@rmbgtK$w zmpJp7Oz!Lo>hqag3G)12<2ht_)s{)e-n{BO`x<8e=C<=6&TmnEoAN||Q=Ijwn2T>v z_7B#{jZ0VTsOr{jknWH*&J1pEq4x2G;`Wy|oIKiCr-z>B`(E&y^)dCj_7-4+DZmC(fDNVq8w~X|UZK9ms|CUDjJ9^PI5wDi z>U8;#!PK7G{SLDHZl^|;ozI(*=sogDvgk$= zd)?3t_Bl3orvBO9S?q&Z?1NeCgIVl@S?q&Z z?1Ndsp1YvmjiJ=9Uhi*eIMu(yJ3i$Pm>OLCKGCHU?mE~!aG&(2jnwf~?17ofjO@Am z<_*Sc@Hf!=1aT7HxxD=P>82!O?CfAn&KSEH;{nFl!x#@T#$Lv#_^-nT7wjC3teBkH zA@mm6c0KVp%S`U?!=0s!722i`)(<)UP?R_??zQ`XFBgHR$+<#uJwBV zj?2K!81wDBo4-*Z#>KiY?<8x}$-KLmcQ^Asz`T2ycXSnNLp!~+ai|0S+D_cjLfRzG zsNqEa4%L(Ku9x@oKzktSjM4j?nWO5gIdk;>wmHvwv0%}ZTf550`vXs9lZ*pv<U}|zhr<}u#=EGE=eE&RUmjFU^h-RKe3ck5%`3sW@NS%TrKdHV z1&{lv6?~!m?w^{^pWjvH-`Hk;EqD=7V|tDY9X?vdX;I zPB-}j(DJeLIi9}9S*Zu%9W9E@hmQ&W4ibN7WDDS)oC2(|VGBpJANNkM9!`#Be}gw& zx<8IJ81LqV*!~;sqXo_$yoWugII*PJ>xv;)JH&OFI3N4@3$f9B>;l&25pc-Wm31}p zNZ5|Ye6Hxse2|_d)@eQK)XX}yAdeJroyvFCE1es%3Y~d1I`bNI=5^@IP3X+)(V3g` zf}<|6cIbVzBl%;4Vwr()uKe*`;4S$BIbdzgT}GB&TMIo2%mmk2yK3;8_`A|*ku7s< zq#Yx3ur~5Nm>SW7E8myVmMe4UdEvghwyN{Bah&kR-}VIgz`6a+rH&tYjJN2$-4=Bk zrn1MJb|2_D-87TFPSj=FAAd@0mEpN}JNc*zdo2`0p-H z&7iMs_Q>j`&>(1mu|1aYnYkIeqsDX>-x;G~LtpEl{!`AFE+sx!V^W@6962J*yLomY z^xzEM9nLfIyvA4Oz<)mP3Z4cA^mWBZ>){PPt5p5E_eHCwQg@@F(Ax0~d7l*xH%7wb zeddw(xd6WXF|AFL!!y=<_^s1lZf?&)7nY0}+r_?O!E&AhOX#u-%Y)?ZoeP%Ndtq4z zyaY>lq-+!hmdm&bmi1f(%N#gebrv`^_{nvZECSAkz|Ef#_vgm<8eZefPkX-8p&cex z6j;Q7!;$;Zy(Kd_&z%iM->~u@dX}8VXUSyME)3^ejQmFa~CD1ZM zSNv`D8h1%3^)h>XH*(XzbJaNaIQ5GrIrWSFhWfb|$`{`M!}I40o$!VKSLn?b-}2C# zI)~mYyfAuGMO#CmHxY6x4bFP{yA>KV7`^%Qfr07G55a4f-XP->6L7tuH_K&T_RyPR zcaM1b%cVEfkH0hY=A<*Gf#}U_@a(Mg#u?vy;P28E?Z-DS8kpXUhDN99jrG2_MQ>)i zuw?J2VF|r4u=LQIUz5X^yLP#-9OZ@O1<)JcS>TjQZ#oRU2_VztW#yf@II*q&J-1O!FYA9bQy?Hk|>(N4J=Xu0fWay1uFYURhl5eu>nfB~NzS)I5 z(~exzfqVnsmYgGbCtc5URX*}fKJraI@=ZSS4LK<5$w6t(x8)n&S38b;vu_~z<`Zw) z^k&`%$p>6YjtAG;`v;&m)6(+IJ|o{WJMv8n{7Lkk?_t_X(;J?5z2QA0-+0GQQlY=t>v`mHl2f59`R{^%<<+@UvocJE8RQN2*nn@5Q) zuV~1oH$Rblv(u4pc6sHSd?QbtVC-HS=QJ0Vj2U`k!*Zzu%lk91e3Bg1bHVZ$F#~B> zE(2bIrDT~#{shZ)Ts5~Qu7YI_oI2ke98L|?9`njKwWF-3s)sv1momm1Mn^6in?gu$m>&TyO zB7b^4`P0qhQz1je@a-IAy?a^fL({Qclp@O^Lt5m1*z%mc?ordfoi9A&kI^^!<6-{f zuR5b|qi2#|V&qGEJZHnvA<36Bgrm}1{cYXCn;D_h{f>P3Wke0(HG^g z>3;2A=#Ig$5p93oHMIDH4$dTyy}!QNjlIJ@l<)8i$G^JDGbWSE$U0v0LG`6vEW4J^ z2HB5{J;L_s=f+q0pxcHAK<|y5^xN#12J|w+16Gi;Z0Htqbmx@I13K{Yr|A}Z+0ZTc z?m1!_Z2C@2!%pxji@tlteIa&~esToQSGKqZoKM?N{ul8rx~~SffUEmzfrEpq+uRyAcU~B-u61*9W@d18 z-x;|NMauX2P^52|6RSY1{&LwntKVbS`y9u3oO+)Y^-)ysb3=V52MQe(f8r2w8GOI- zEckBd;~hhzkH^3h7w1?*&YdDI6P$~UCho+hj|S(y#99m2Y8acrImVKUbK5su5PeMJ z+-B+=JM{5PoGWBZ=ZQCcP3Mm{S@iLLMxGlGPi_>RAa|Tap4&Hn%YgFSzFKS?E?s;) z?$E^xkmo+@;s)}Z+24v+hVL2sqAkxE+&E0$p1}>qV{n81b8+JaFK!H7p0jZSd2Zr> zxZ%oQ8F}sqHKEDd*tVjBREip504D}&wX<7M0*w?hq z^Rcz%<`0?mZ_nUgfs22N7^ZvVOX_w8m6ZMw_RC+_Atk3Ys{?Apvzc5UNxrcZqH4*vNg4dB62u=;9lJi1OcdjBP zX(G7Eo|Qj50qsjFuV}S=5UxGQ?c3-c23KEs{lanevA;ckT6wpJ$9nneA2YvbFnmq^ z;oN+6{{BJm_2mq{{$*@va-_jmY+ha;!qqOmf}`la+lnuz9w)riqiYy^-AIn2@U({U z7<{GwTzrk<$G-qw!^T&}^>5(9nfU7F1!d^vqttesNzUtvh9ZNfHORsxR_JN+ik?nZ zVY5_S&pOdb`wTI!uI1z<7~K2Qw}E^47l3;M<`=p4zW+t}u3`vySeFlL04-b99Q+%V48GiOE_^8;8onH6jkECO9|p#kFSz)^*bKff zmRx+1u(uQtzg3Hyq^g3Tr3=Zy0)2B2J zu6o2npBfz;T*Y-Ye~itXlgaz$oJ{Ek^qI!NjzDA?cu|4=R000UFQHsfo%a(fbMlG; z1!v^q%>o~1N$O)Ol zy=hizH?izi!}XD4oI`5P<7pU6oBm#O^|?9|99XWc3zb9?i>*|La!z-Zfz=h_t^>c-CF z{VK*&$L|aI+(GU6U0jK6Q=5tDp;Y27zHi}rH_z?Wce`%rW6(Np-O!iE5DPMrx}&^P zK^$+qlA58s*EgGM?6#vw}TUvx2tmgj%9gi6;qz>p8VVH|VUF&B|*FrCOjPkBN>-=2RJ&FR`C~$ZK-82*+9tHhP`|=I&(fvLk&d{F8g`&-gyyLbPqrI5ZUODaQ zH@7|a`#|lv?_cP6gx{v_>jlmy^S&p}C%r%XXvlY72*{&K)0Z=IQ+q5PR|q1V4P`l_^TX6@Ke>Q2r?4v%0j6N|E<`n{2Q>Ndpi zEmVwV%?m@R@m51{xngoSbNnTpv1{Ta!Y6kJs(vGT_eREd03L9OKa@I=up-U=OL~`c z7X675D{`qH`tI-t_1_9V%kBRQ)YmSjf49G<2k7IQ^wF2oN6lNAK92I6Z8^997e6xj zLZ&v^`1E{5hZNhK1lJSbNPIlsOEPg_o7m%<$SYAS&8goLA32`=4~A z$xj^z&fmaZDzfp~{XJS_r8>t!@5$>Fz84Xfwt=xGc)m_~C_Hb@@u1CWeh>M{hohfK zPE1=B&u;`yH9D`HxGbGnvD~`#mN@;EvximVNMk7D?7J9e-xYJ7yt&7-q6PN)+-BA% zy2@%bHJ%xd*6I_&rBxyBQOB9_{G$Um+aBWB?@Yamcco!=Xf^zK4fUCU+hO3QJgZ*# z&cQ}tw+t92sQuGT?Vnz1|MXG&=Qy>0UZ?iYDb5e-4+Q@T|Jskq+0pqy*}QrraYLhs z)4RBB1F%+}W$!BZG;lcrO;ui_yGZMD!In+A?-d-s1k@*?l#wrBI| zktQ#$gB+j^{AvmCp@{ujPYmG!aDF~#4>mhz59;1-?kxx2^{k1`9;|oH9&GX1JR#wn zJy_4#gRzUPxflCxFa0alO22DO63nljb<%Tng{-qP-(L|Ut2jdC8f%Ulz&pkJ>YYym zi^MYcw>y7$>7S0yi90lV@S|ppO*|v~&ZVQ`M>}X=ZRi|pQ|mnu4t8~O&QdSuEcJ2D z(s9mNdYyBYPKASq`olq;^LWIG_ZzgfJ8Prb6wy+695gRo-<@?+&ZUia_L$1=(3;r! z-`@MV=Vbgf@p(o*TI|S2k4!Q(Moi8V{1kt;kPzWhq#bB+l#5Qy@Wd3OR2McKXta3QD?iJ zn(Nd2t%phjtw&1iy}Fp(&1gx8cNuFP^#B)AYkM)ZwwF+AdnvWH@2A%GGHPwtQ-6J0 z1@DG=HUeyx;w~?-0KVWJnsu0e8Cs&-R}#o zxGJ=NzGdCq2^_U|nn!Ym%{c0%uVD^Ttf#MmZbgCPMCf;vb=g49a11!Ox%OT@5d&ui zy3hPl%BMVmEESg=vL+O?)b5nf)7A7HMK05xP@bdcpZ1DkM-NE$vXH&Fuk&-fed$@z zP4B${pYyKESU)!HKx=mZI1~bh5x`+Ia2V?g9umBuorSsUF}8wt!n{+?JLSAn&O7D2 zQ_ef(!DbInvgxaxA32=3`rPvyy=OP}bwF3wvrY-}AzN6d&8*WF)@dv2)JVPnc?3P= z5ga6spqD%X_QP7y##eoT-aXK$Bkj=XUC`;B$f2|qebr~4vFYT+z-DyYuU@gw%sSG} zUhdFdweSBedb_FVHjn<I}eI&Pch7c#x6+a=&WlEv7&1*Ckw+@>dnInFG_jP6#W46-gf(9ZV`aOGEZSZ~=8k;^di}sCN)@bJ|nz-iq zz+soovz1#HPf)L?1sb{;-oGUT{6oN>IBsIQdx-5mNNjg6vE7CyzFHnE>8G9uyiR&U zJj@#IL_YAesq}RF#$sd)_+&sLc^J44a3h6Q2PcY{sbM9o$UCgS22I(&(roK@YG#D!9g$(J<#)sJnI(ef`6ZHV&c}WXs~1A zY6`8n_)vojz_IpW_Bm%0^RS_y_e?@Zm2Ud0&!1}Qm;gR-er{q8*!&ir!m*491t z$@g1$%(43nm9Fi~Ydz9#p9QvWO%AU)8*SS;llN|Gecq#E82T2r<;UmVB^iHgT7Ep- z3@us@jN5%d**wU_AU^`S#(svEq`KBY*ILN)-kis|5qZIbTl0dw+o_>;Klv1#VST4H z=Aj~g-Z6OYT**tzot*8&l2A(LK1aE#w#4Y8$m_gw2s}RA%(}QZ-d-LQ?7uKo^5Yl> z-$GB0;$5>}rm=qJ*+APj{>zacdB@w9jdP(VOA4&gXB@fBI}XKGx#Kw8ce-hyF&wQI zj?4@m1HY52*<)+i1MAphEz|(pObxItGnxBL<~}ocgz@x&r>g&z2wc+kg#pjn`p8LJ z2eave$<=dUp|O`7f|i4a8D3~Vf1)GX4>$ojY-l=ke5357&~()jD^}h)G{dFo&E)3^ zUXBd5R{TUVm}t5i$Mrj62+}ebG| zCO9>f?QusNk)zOKs88#c{IsDq_%lX3POudQd6kA^?LTBi&_qXuhyPy?C;+(AAM~m_<}z?H6I#eJ~M|pXi?)^{W}u8|9kqa z#>QqfToak49LTYuR1NQ-*9C#E8GrhF9^c1%ejjE9Z8^uO{~&#DG_(wP<(2O`y54B& z*Ldb84cWV2zlGl7-LD6iq2IAj4>dy9*{4UQaV_QhbZGig=F9o83C@R2az1P)=fie! zK5X|+^u1l^d+q3ZT8tEwPOo)R?|oQwr;f1=jgELLK~4CeR-i^4fOzGoc&tN zRefs=UL9hhVldy<>lWIJ=~^yEC$e$nsBnyPWfPn$o8(;CPTKC;j{dU~ z{bv{YPdoY#{6zNPbp93itaE(KK4#y|PM@Xb;NKAT(jCP9xqE53^70N)Z*2x}Q@u5J zFHtw?SL7CMlpewUF}0)MW3jt<4%tDqal|_%GpmM-Wa^l^PgHxHeKIU-pSb=6XPm-; zy7SXD?sReGJoJnu9-K1x)M3lFJvyJfBj-G}?UE6gYcn!KJ2*859NWshdhm+1KDdQ> zwt!ol2b`csNcIgt7ZT9Rlmck6v^MHuUK{4^G6}sUwBnBl$w|NSOEEKp&DFNqD2* z&HUye%Z#GViD#S%_Jlce8JXp)J5D!s0gLVu0YE7D(b8u^OTk@ZTE^=2UJl}WaB zYfCAga+o>KO=rCwtYa@4IO7@-*e{~7+ zEpU^aqxzVqwv@BZMuu}fr|FUMCf$3^7`@N+_g~T)XH86=E$w^t982~f&REB`92rGT zYd_yB_#R&a4Axnxi8?>`#0` zJId_y;mW_`@R^~~j3VX&vSFnF-9FxY#%Fc^CkdEd3E!iP3;PIWZFUPGqj41R0_QUjWSDSrfrWqsw6r`6jVD<>(nc`LL*Q zXwLeiwxOQ!qf<%Vl@1hVEVbBk6SP}I`wv>Tn$MCC)SvEa{qh3VQ<8CwEgpMd=gUt0 zL9U`zpEyiSIQE_Dc)4|AB!4uJ=d67%nr7rcJx`nJcO&(NbX6Zwr=44|4>FEpoV77G zkqD(2M-W?2X|k^}n5Vi$p7+1(vmVwnl5up6($)r>2JbVq1$b6;*z_lw!1o&%r>kSy zbl9%fC>myL#@!B`0>)lCTmc<^ROf9r0yk)}1D_;~eZgs<$?TIh;a%cIRO02mZE?o(0a8f-^W!p%^r9AOsGW zz2%HYy8lr7{_OyLPdiKB*SUSq9Jp`og^f9DW^|S^e^BdX<_UiMtGC~>iXbe$wZH%J z*4y0kyxXl*K6Z?*D?R6VcOa`tUS*wPd%^YRt<(ne2=-Yj8S zv+tILQm1~CYT6)MNoq< zjFo;qid+z@f`5t!*W3Id2_Nr7R*5cU%(JbOt;89X|hn%ad{>L`5r=m@U!xQ^z!jO$p&3Oo&5fu-yKec;EO zw*ICBbta>XCB|5k|CB)X``BfindDu6?#rj^y%)RRwWYbV#nG!h@BP9md}!F=uk9aJ zY(-iZEjsxlYuM^CY_j4pst-Aw`dT*44z&G9_~)#0w~l+Q!*?aG8~-RamA6J;p0?FQD_IMz@eE*C%Gyk`Qk$MU-E@iYc`Em& zBcnol^VsY8$g0s&U^#=avoFl~y6~a{{QdKGVsEB{KQ&gQhkaC>Z_B=i@C)1e>g^|Y z2daK!{FAJk^wnvahkTQxL#hAp=oYSgpz+>Otnpr%9erxC8ot8$SYEz@40&mYIhHfWY`?)QGv@yOyw)iGa7*$T zy4WUa4afOz}LiVug3LG_aA`2XU1ypM;kxfaX3C;=5lKEsR7{_2PU@xlgEeI=L5Y;-s3Nz z*YfqXlNUJ*7-0uC`H~gDN51_qF%7~6*__*XemOcF_a4wa$ujLuo+S64);-BSdRH<< zIp+s_^C9tiukXno@8c7!7hmPw+u2v9|H;;|AF2Ndnf{fp**?+jlRB=nuY65pPt(6> zb^CSc-%bCjbJ>1Frj6Y3+%U*^gkM@?*U#Jl50Rf)GN*LgXAYij5`G_R#gv!0Vxbzg%?h$>*%$)EKxxnf43WEfQOxHJj-d86&xsJq`?%CmUHv zJlj{y-9#-xcT5TBaB@7nm39rxpxMi>vq}eQGj@yJ=2zV|k2`I?UYRPQ&54qC7)OkL zJFvAxmjMrC1MvybuMbc&wfZLfz|iGJXjmOFL-V0w`OvU$uz%1cE;ah6Y$~)N8Kvk( z_6RnSeD<&Q;`JACh8z1OA$_fdzBc0%bI$B|=qsn2YQR<5mER3qqTrG{o`sC30$7LH zgPL2t_Tlx`TtEHlypP(E{^z6MT43TcKhW=-|9-6QM$Q1A{LBw3xGEMgPMiidms_Np zyM15bUEekIw~kmwWK(PHm`HIR@~ubzez*1quy%E^D6o$K@3G9g`<_!xKJcWtVY0~! zGqTxiZ0O+0HG}w^jE@N%5FTj1nb-{Ap9FT2_iP-@Z<~zI!Sy+v6kKxPmiJZGianDB zJ7*oQaN*|A&wAG2N7(SP;Ku&Sg&XlhPgdNx;s>?Y0Z#{Z528c5d`EQ73&*|8-^EGS zjxiV@S6jASCp0axu~2#w@ve^E)F5~afLBIF?XYc)IEM zQwe;+7XTk{P4r26f{kMZZMA|4^3+6lt65j(Tg@D#pNk&}=M(IYPT?kd1Rk6K+lLto}NX|Hd_S`byHKfo0_`cT(t1yDytBFH_Y%V)oC9lepkjC z7&r#n+7802%4~Q=fm=X&miP#G6^G7MOJ@VuW8k{6-J*9$whsL+gPYzpuU5T+27e^k zgA6(iy5!V4*g^hhF}k zfo>Ze1WteW?+lz2+Zei9a{ASgM}UuX;^ovVR&6;yHH=kT?w&#JS94!HA}vRZ6Mwt9 zt>Bv;IbvKW_4_yNxKrDo=EV1mM}7!r`#-X3pCdyYfzKbFgYRQDYt3GXu}D@Jap}e9WzR-9~(pv7JC)%$iRKn3~rn*MNOf z4eycd+5wk}b}1bBl?WzZcjDpC?L|R4$8j z;IK`Dygobc{a<6h$c>8(P}5zt-&G@3@NxV)+hkLe{lx3nxttotu3sm*hA~Y6cE}it zO^)OLIo;ad^eFxV?NjYHql0NK%!4xzQ(q8XtOKv6I`NSB+`I6(cjI$EfX}@LpZmd` z_}+KT3?6Eq85~kPBz(cd{;;33;vv_A+lq(uz$Fg7kKe^QgVS+%RzneZDPLZrKXRED zp4Q|nc*c0o*iU&+vQnHmh|Y?RdF`egcJZ*0!Jzr-BQ1XzSTRoEBRcX7^xD`(;R)XH zNw1H=E218lmAEjQ{zrgW<+))t7%rJTd7l2w%U3!3cBnpzl2B@d^aKLwxVr+7>@dhp)h<{bO| z_FnN+j}O8bo95Y&;I}Q*M`yoQ@3tc1<9c8I(-*?{1~v zu!D!hk#+6FMz{+bVLLX$4s3)avx2=-W(5yTofSMhZC3Ee^jX1ZA#IIdJw}7qcZHyH z^7ZOmlmdJt!;IfjzRVuzM=$Y@g5MGBe(AVod_98aZg(#`a+dV^cpdFB_V_|)FB5x4 z`;kcpytJ{z-pkH=x$ufJr-AmeFQ2{4UNmup^x?uZTE!gb`(Lz|D?NMp?rfj4Vh5tL z`D^BR)=Tie(S_mcw*kX>?;H%H%tN&K4eSr@{EXkqGv?!QY%Hva@u$3C$3q&L<-pTz zqvtGb#N9S#hEmZ1+Bgx;j&J=>VulP4UFY!7ufanXTPedsmt=V8r^x3_^U!*J7Y}XZ zx{5!|VKrA^uXDwuzlz5j9@;P?5~I)WcYE=~@jtlpIuF}#d?j>kh_>GsJT?#4{(A?q zd~SS`bgkXU*Zzlr8MGt>ZFm%U!CfcGY|9lVcH`*C7cT-{Cho?LKSJi;X5|etejo3B z(QL_2|ETd|Un=C<=9+S6vIdQJV)`*DfU$}x8?r0G-!;{D5%DGyjk2vsBj?k~2IT`-fXxAfq z&eZqGoX0F3JfHcr@V61Uexqf_ z;ye@ZLD?PM`{I$N|AtACJJdh)NN21VKPa$?>sj8BP0p6HtTrx7%R{OE z-b}tJvZ%&k?vWGw$d9;J>bW;DlzK(?rhD$O{_la7N2kFR0R92moOJl9LX&ylR@k+T14*4R>(FcmHX_0t|Kl05d!7*^s#xc8&i}!}s|Rv1bx9%#P| zLXV7W3+zOfh!5B?i}zn)9M96Pv5C^}8E53c=T!&(fgSV*ea@vv%8S!@?$dajoVee4 z@ZI>Ea>irqP^?#iu_RgFa^&e^a6Zhnj;jSeNGJIQuy~d+6`@0R8ks_RtAk&RYc69N zXgo>QL}U7#XH3}H%-L?PEb66~x%B&-;{!K`GQJzmF}|5Y9N+N4=pXw5y0=60??GVf z(mt0yLi^AyM@Al0el7dKp?zy}Y2P#7K0EEJnV`JYk!jlZUy9Ehgzk;@%xRwoH(hui z3p@1~4gDLJ_Cd;yo2+8pkh`SYNq#kUK-ODwbL z>}j=mn6XPXE!Xq>US#=tYq4XN)8@oWt<^E)IN5+iS6q0;s_ZzUF9J_}E{2B4Pl#Ne z@<9u1nB8%5ccAJwkK*@zoVNEc-VL-_Mf-mEw(S4V)P17qC%yz-W(%E+Zc$1U-;H?Yp0KasU-q2fcCk z_m>(oG)Mb;(H{%nfWu~Z;@icy&`|JAF@;`yqwjO!+i&pspA+9M1>b1f;2Uk8fp75S zzi{yl-X*+t@$Fw}-{2eX8GHj548FnZ&KKXdD?Z@@@a5ch~3eJD2ZXi)~=9M`d!g5 zoIO#{_IJnvS-ejBG)zrhhyVUWV}qaLr{G?B>wmKsT>0FtO@C&*$Hr0E3fPy5{m_0C z-xaTY<1yrS{CB$<-$4F(V}F~X_mJ_Rs}0aAV~4}XJ)Iif@?A|*jTG!~31eq-^GI#~ zezcTvB2(#o(Yso51Rf>UN%ssr;l82y@HKJ)=XUb^;}acOWsJ4Dzdz8r7k_9aGRz|K zW`+%)9_hzlzcY?s|de1e5@NjCckeD&vqdj>UYK5Y($6Y zfS%UT=FeCY$)xTWj|d(-BVUKT22Nas<}e5xj!~=kTyW65Hqnm_2ixXj&IhL57YFH6 zIzTSWe=~%>jBWxgmt1tMi^rqwb7L&$pNlWg;YaqyGsTcil>_brHqlDjX!Hl&d(y%3 zftTVz#D`tKr41)LKBf&!6s-idvza%%EzwvJY`2J^rM{N*lq7iANSg`P zZzb(^f}hvU#Agrf>4ulAfNvTfKXQRVsVgA=(yyr}t2IjFaHDNA_|kHR9*-6tg@Shu!2d;$zh!E- z4T1h4*Q=keUkLq^?GpNToUsbthW<5_42k}IW+?P;@;gQUMhuPq0SlM@eQ^+4G$0>v z;P8_nz`@Wz`WcA+eSDBUm;EX6!!0vYy4G!;nR024jYAH9k(}tzy55Eij(lwp{Ae|2 zA!NsIX3;ok53x5X#d`n$g_g}HCOemw&83!`OUvHW=d;kVr-wqzz{x>q*}n6jW#nCt zcssNVnlmNxoO1bcXxTDos&unlT6WR5hD6KOTGl&4%ZMW}v z@{`?W)YO;=M!w;~1fCK@rj?BHW_?DU{a0t-8v7|e!!rMv-g`?GUznZWbg&mccppCP zt4Qh&~Cy?#Y@O}no>xUah&@7K|8I+up$)y9J3 z%E!0sKZx&WEp@-{b@gq>Rw_1jBmJ1Q;k_CY6B&9E$LyuKeVQC3PoE<^eVTUZPuKah zf6aC3!Dr6|gIj z9N1_XSw`|^w9ocmJ;Qn^u0pbCUnB5A-g+0Yf$msr|5kxrhmp2S9tQ7f9iE|%pZrQu z>`QTAA%AckFxPplNnmW&lB>r0t5-9AwHmXQd1fu6Ven}g?}O;|Lx-jXk=I zXYRLBwd}pbQl3SwjfEMDVm6mK@+dYf;ezqO-OoH&zk7vC;7M#L`%keC9rip1np=V~ z)*|OgABawnt?z35r~bD0e}TQz&b{X?{3ML=1+M$8)MNNCR#2;h+DZ6<3nIxSTo;=h zupQqZ4s0)e^8>`#K1+;k&l1%YvF;#*j8i!>F~%CBz347(b)KV7i zPBb~APCSkS`($Y-^&oaScbzm}=_Xa+>p^Ui%5&EKRKAO`Pt$(zH{=*5fJ+IvI8!Qv ziK&&r?rD|51Jf(X4Xz|Nn0%Zv@^RRshs_?!H}K5zsh5SE{9uni9Xz@CRdR%<)8`!U zY8QI$LguuJHD=9KoAVlQ4*AIB27te%p_Jz)xdlY)8S~T;inOh{i z2Onu{1Z!OfZ#<9MXE%JE^}?qb8^LiyO~)GZZGJmP`NV}U{{0(et<8Qb zqCH7Xuhiiwj&PR z++pAC=&Me=P8B?Nd8IX1ZL00%3k0L=T;X4gv*vER#JZ`BnncC;5Tdk~#lv2A@@P*n zXVGT9|0QvsF5fh)2t2Z7F7-jIPAtRAOpJ)$6^v;6v;W0!d={)sd)$+)>rUcbl53#v&{^ehCY!ic zab54>CHHf$F)xx_#`XR|c*(M}@Dk5Blt-AvH`B>@ha-20x4QmpGd5?uA7Z>|y-&Qu zfpHdHaPF0nU%v7EL&KAO#Ok?gat59R%Clv+!t&II!H?bK$+?zU7=X>h_tI7wOZ;Zi121 z=OaUyPcgL4;5+lVocU}}ZVKz4yQhpj&C~WuPuqGv#@>&!_wTf?*qS&hi?zYN0+A(W6M*?apkGb@#U$m#O=ZE?%RV0dT$T* z^xYmjc>MNYFZ<^Zd*?9oJi<8onByCU#*P8M$F@!6%lyc zp#)=8{x5sT&i|#I%pT%A9c1J?nNxZX%|3GvU3!*1^mqTsUZFpG578d|X%E%E0=_xo#C$Ic zrT%7bN_k49*AN@O2|cnCdWXKbcGCvxDcE!m{n&p7-J6nww=vN@@jBK{G&jlF^$XBl zHwEmva@F8pfBj|n4d30G?rV#uudP<9a{_uUvTY}_o8Eoan>WAHp}`-%JQ5oY4dxtq z*N2#wGZyi56BoJq{Bh$sXki*R?m81UzH=6O^{zp1<0Nz}iENT=W<0c+L~c#q&2=+& zZfv|pRwSEx#+Iar-^(SY+y{`Wl4EOTbYi{j48~4(+Ai#HrW(FP zeRXxy=g$JSpD~U&_+w%h!Jo9OS}&5hVzh_8gRP9%N&38l8qYCkk0qX^8gX7e zdLFTp>y{z63^8`{b55Rz<^1!;!?hAy;XRM`U>&meLTopSvEeLX4)c(U68>PK+aFB! z`h%T){$ST}f3W*?KQ*%b)X4T{@;j7YMf}I1BHkU39cY|tux8imyU~U7YzNMdWS?n{ zUTNUGy%#uQf4A>> z;JFvLcCa?!d8~zXQN6J(&?nJqXmr<3E7jdWy|KO28+)D_yDv~GE3RV@1Za^|3r#KG!(Jr<`#lsjD%ceVvzQ-PFuDc8gC5|M;6k z-fG3k8zS*K=3a>02A-NeR8#jZ=d)_+B8#|v$HUlXIdfdOVzC7IKf()SfOr@`o$OE6 zRm9$*ufPYq$m&ZK__auBun-_F>D<4t?(M%u9NQGq2jqtsMisKVW@S zr;V}+%zrrWhu%gxppDBXRKW@vO+PkLhNpQ#lY?v_;dw!z4DPh;-&CAiY9*e zib##}eOov)#L<TjJ))LUFDx$+%al?Yo;M>$6#(QINw zgLT;K6Z{U}e>xG)JDHqgJ$-7aFH&D`$u4-(l@CA5*b>m9WSHlsvJdCc4*MY%_C?}_ zUw`uJ{ckkgc7fy9*)7U(i6JjU!R;70EnGg46Z8LDVzCV0T8GV(b*{MmeUbQLD>Vro z6^DMi^t6iH#W?g7zd$NlPg~$XypiiF{=ohCYOdNBYq*BF(w>VOXWLtb<~n%+-&4I~ z@7{vH4YaqubT>Jh$d)EAwvzG8rcZQzd?n|X7YnRq8x@`^H^#evlZ&poRWXUh!-z|~ z(K_}X#rTc&MdmN6zIEkad3t3ymmq+;=W3Ln{CQdd0zUT3|NcJO_kDRSOyQhxv4r&reM*Ole+g<-pQT!@O!gWKCaM! zs+-^2iP3!qJ7ID<@=Ymnj_3tzY516YEA&}Ntg8A;P7m=rF-xWB`i%cN`WZfE*y<_y zf|xMd9$%2Tt|hbPsVX*9OQBAou;cLM!!O%wO#`UWr_=;ZLAlp(lH$XU-U& z^$FsF>2DMLb+YF2HFeVGn7mV1)c7Mt~H47&M%1+8(kz5V_Qv(ZI--= z{Y|#QKQJCcHyDd#${%nBM-;xb!C4ot{I2+#=geB1c|K4a7=@svYRlAlt`D^KmtZ># z65sr}agijSMK>>DEp{V2Sk#buncTnS1$NGTOttIwO^670eL9zH;wUEu-HjQt&*xzc zP0ZHkp`EvVb^ghf)Qnaxi(t_;)-rL{=JPl{6F+7?kLNQmGM^d8CSdpTH~V*V79rO% z&lqbgUr4obN*Zp6{Dg7I@8Kh#Lu=>8d?#3E?e$~$@AbJPKwK309`Jbijd#>h6Ea|N z_Qg2Q_dLF%Go*hX#`hA>_c45jpQnExNzHfQl!=wH=WXb^Cm#Ant&@|3k)*%x53pXx z!nQrXb-MEFv+el@PoZ=5Gk@xIs$S=jFmyD*_gA3}$HDa${O6nTpKmD)c5W>Uc5N>t zzrK+Cdi>|@_|FT#JtxQh)%o}~(ZQm9cAIKjK67X_>v(2v`hC#CbZ&YdxO<#E)Q)du zGrksRXu{MdE@V^fkX#d7_i{z9iP2tkBDiJr#QeyD|4h@_eVKXy_BeZ9A>U}DFVa&+3;;FY zqTpGK{jNC{`S4evM;$1GCcR>%UhD8jPLQv&-m=EL+4rH=H|wkyfA7qp=SK2;SA&~N z!Bh5S@_uL&JTR*9F*eCtQ>Y!`nalGuWOZ!2rG z9sk!({9n89f3aqqrDLBP1>bP}o9@{tar|MwdzpCJm)ZL>7|VX(P!CKR*@KKpG_{j` zhYxbC>J#hg^RI9Tw9!cy%`)x?O+W1dya3;0Rj zo;2Ee$}jmG91HP%1HQ-mn0r?vw3~VCy_hkrvFUvbe5%GbN@!$c7_pf>)ML8H7fSsT z&#z?ty4KkAUNJA#`Jq$;cIWtvQ0jJ9X89QLr^pBg_7h9Sdu6O$5Amqi`$E*xr7j(K zzj9c)&fK`A6Z}y-oxn8uGUMhQ$qg;4Q9zq+UusMK#&_{-^aWt)JgaBKmo`(=GWi0$ zkGZFHNygd0TnB3BAn^J#`oH(kSztyj?Y@QK;1ZrwO-PMXIMLrfy!GgDWU4v%PKgoj z14sH+aRpEMR&xbc`qp5#5x%U$b|ai=!hR#XS&t1zxYNvYb7-TKb1N=p%*5IzR#gO( zt1E(?Ybt^VnX9Qc&pZz^_an?R%6xPlkA)9lLs$O};wdln?&$&dKJV>@KFelVt+-Wa zQj9i@--dRL4~6fuUqeofb#aOlc3#shA8$It<<3FM#L z>D_B1*-s`1(7G;?T*rQ3KPQ{G0z-73ZR9Jax`1l}ICcZW1Hi8b*c}9Jy};}c@N#Qx z^fl%o@5ql)fG$#K^76YmD_nW`2d9VZ8XMq`;tQ0o-+MPQ(E-lCmEF&weP{Tjo%>UL z7d^BXnPoBc=tkbV_~Z!Rh}F$LUvKn4|J>{Ixkr7T5WL3Q)_^c~^^o)MVJ{=E^nC4E zv%LC9qn&SdJ@$%Rd2uE(&4~^xi{4)kA2;+KI)Ic^TnzYt3>F<@oooOKDGXnzlJ;FFMV17rNFu|XhfTd4}Xw zGvm-R=8~Gk{3q%=u$)AUdkMKt1C7s>!L+XXzMPT2zQca7;}wUQ_=(m!*;6mdj-ObK zoVAAdi6!_>8%G2a_Y*&{%$Mq{_occTeW~t}k<>dGNxhSi)H@j&?43R`7+Y9w;wMU| zy{>vE<&!u|*x@hr$Xt7hk+~Mz?H;22fnq2Yakfx7d4wCeMrqIS+ZeelUR7@Uk?YEB z8p!vHh-GqlN7oYkv{ODDj4sA!i(aGLbo;l0Hf(|qbAIDf(Z$d(eqU)BURK&U{lls$ zdyB@d`q{P6+Y0IHPG3>3rVaY+`*-!_*2c7B3o8_#z`b-V0MExXKH8h{67+d7`;Goq zl7;9rOKkhSAM;7|t~6n7~7;U|nkeTrt(+~CNwz}mi-9bfp%Qx7+bR(D+F$i|*Cd(-z6 zOX7FzTlRCZ$-A~;`CQyJcIABGlZh|p{5c%jB05Cln`M2pxA@x>h=Exjx`Va&`<~a^_%QF;g;w z4GYO=o;|Quy2(I$;12J2StG&W5$rpI;mO~;$eM!-!Vho%-p^(%@?)ImSgsvlEDxMz zEMFYLSWbk8XYs*B&`rY!@AApMw6FLd(0isrtEBf#^U1!nR(ek_`F?4=r;OjBpV*!8 zhw-O9Fqo~hmc_GwM*R`&{lILw-^3G z60@dQ*yZqs2Ju&TeS8V~3;O@(+u`@Tm&@-Tao#ojp7&o1fBag`RQ68CM14k{lwu6Mb~YICYS*ec5^6k1P@&WNe>vo{2NI zIAhy$G5&`2@I>i`&Crq-;O?PgHw}X>Lc0p^v8fK6_qR{}@^n*xxW1Xx;cA92EhkoK zro)$-$-CQw|LoBKwBNe*mdVUTI)msrbo!~dWSe@{bD5PAU7YXa;fXFT{x|d&(FmSh z?$E_QU@OSlcguidJ@bU0@7%|}^L$_OuRKeAI%E)Y&Y}8EG|om>nLu69tN6Q`KWHYj zHrWj>g$sj;dOkNodtc}NDejeg7{4F);d_a0u2Z@8a-GJtk1PBwah&T`>Ze>`?Rfk- z`s8zRJ233F_0Fy^GJGGk1&*Vyy^el-DwOI?Aj5Yf!}n5;zYpE$IJ#aTus}A6BbT^1 zY|He+pz*_z>n}onABkK)iWpPsZ6Vtnt^$BF|eJ@FD(KFzZ?Dy4*xrXj0t880Z zl>CPc=P5%#iVA zEd_V(XAVozIoZqi65IbC>~gbx4?KCjZ`t>-JKT4#?;#V{uXujRE{0b*@U-Uwt}&Nx z=5m1fX-&Sr{&ds&x3GK_u z$&v8#!vFk5_F3L8PH4Y=78plgW^G!-6d+I7 zalK2(S5coPj?d5~$-u%>`wTSMXug}%w|Ac2&#|4Zuiurq&XgC>f7a^E>`Tb;u7UST z-zk?}Jg?N)#bd0ETZ?vs(T%Q-Jo8D{en0f{FE)M=-DQaWnO~mv)@uzI{pJqpjGqYS zXX!W5t-u?;5}(3#32;R(aCw5EJ<7+1raZ$Q6JL<;OzSOp=2O=uK@3T9YDQjO0G&|$ zv-m3amp`M36*nJeWt>parX3n(w z7O_VwKV{QD#UV9Ik0Rb7Og%->C*hV$0~BNXEPb1HY0KqJ>MJ>(IZiYF>TL^3L?iQi zi=mwwUoGQ^nepbeedu3OO%`V~MyKf=WR6AlK8h9rC-$Q3R~lz}{NUbG)TUfP-Q5+c zhot;;#y1Ig#nC~^Pkv^9;OLr{O8)TE-?#X7|GwA?tkxWh(f5}l>sNir`eKYT$8}Hb z8@q(D`{X;az9XX&J1L#Jd8T!&nspIumOHe+hVN=mwEX}4%FZ1vg9k)u*Wk4CZY}*q z8L#@vLniS4rajciJdn9})c&dV+s%~`Jo|V6i-1mix?m2mnRX`o+ zLQ@A?KAu9=fu1fHNglwb#=ebBx7TwwWAn<1-hL;_cGGZi@=C{gd_v+k!Ue6l#Xqlq zb$CuZp8Tt2)F$X5{wIG_!%JVX^4FgjCx1h>{p@Ap&!jiJHjaJxe)^oP_wsssu)jU5 zcg7lf&05)oT71?`4JHS)47=K8@VZOe#-dN8?~MgUw0|-8PViZ6dhKoh?DoZP8|W*> z?-Oq%HYI0FPdd+;y1YEAXR_>a;KbAa2RK2)cXAbNa_u@v<|y0FCtWz|>fiu<`?|I?KOaFi;)C8sxAC`97bzA9duuhgo~+9Jg_Q7WXQ+ zSHZoXb5D7l+IL!W+0LSy*=xrCUUAZ_8F*VnKL^>P%fDKEQn{rU@%iU`map1U-K&?g z-!b^(X`_nw&wM7Y?aK~4mJR}s%W~lHFCKXGK%3qgo$&`JX0UhIx5wUlc9MeDC3V7T&d3!OqQgF7!{8 z&*}PgTSD9q1vjPbiz5vG9sV`h7e{6D-*$BF4)of(i-VmDFAgT2$5yfz+X?(exIR&I zjr}QlDj6G{a+~zt0%XbPLToDLtT*jVY<#n+$5_MtI_?|&y(n^soXL6K9DDTpWL>$j z^S+OGj|AUK0<1gtWao_{Tfsk4Mc7tldktV$%|o8avl_maH%8;IecXx(kR46=8C`di zPYRyvdE5TSn!L&ygB!AKNso~|s2+U({7umAyP>Iji5VB|e!kqM-HpIu6?0$hOC{C- zk9ElAP1so1V`FXhr4F?CQazh}oS*CCJY90n8b<|RgP)xUjLfnXY+x^lw#B!jFTX-R zJE7gk*NOJ>)cldh1{!S{v|$hPd0)0=1^)MXTj!-#80!+4o=gcd~yb{1F!Q7H{j()Zf zzn|6ogCuiHmS7*%XXaQ1-_Fx#<~mtd+Ul<#+uGm4nCdO)sf%4RW!tzwli1O6I(dDV$ zvE=f!+#Y-b9aeg36kREXt`tvT1MeOYO!kflcJ_@3b{!uPJk>HH^7$E|*26Q#wjM@z zI?Nc0z>6cm{Dmv<;a$ZTM$tF0hzl0m>6>-=zucV-d{ot$|LUr@_S z|LyL!TjwRO1Q1Yn89?*@{?5I3=FR|`5bPhz=QB+1oqNxD&U2pk^PJ~2gX9?N#p}7`luG!DoxU2gFnzQ*{%Xco{kMrGc zeBiDuYjpE;Q@4+pqrE1Be4&o5%|rT@?q38SE~*|I+Fy30PJPb2!?%3V3g3=TTA8)) zmipr2!I>o`&;J&E8lKL0ut%cUB}x93ZtWBLJT)1@(~I~WT-CyVwDoRj5xMcHblw8* ziq2c0^HzRuL0_IP`URHgo7hV$*pE5^A5i(WYi-*#4_h(9dQybn+0V(E39)fKb3^hO z?BJ#4gJoxBbSyxIo9nz_{0G0r9@e?V%jk>uw;&7TZ_2NZfP+HjI}cxBBe==KHjl7A z7N+e8eVe+t>PP-O*G8tnV-ff(L+~NrgUC6(x00Mh_dVAiZGzsLXMZUW-T+?U$I0aM z(f%_!kEIuDW`aX72$sLgum%Zdc8y!)kkLVzjenF+jLa!`kmr#(f+6a^Gg1F|4e`@8 zH~T|(llLtBTR^P3;P>Z;2EB8bb4;vw7~64$S=0MmsE$3qk+Jr=mMdG)S63+BmI)sI zcpGw+za>{COCy|_my#DHyM9YwW5<6GOtZ)dTl4eJg|^~TdSQC&ZD1+^rn)3d-xW+H z$APKNhH1TE;_s7z=}6%jCf*;q?nvDx^lO3g_9h?)C;8%*_SWt%2yN2dTAmXxsBZp` z_iK$v`d@S4#@FpJr(dVz!0qqPD4wvv3x^YF@B3dU$HUpt@QBf-l1x29mUpXKK33prlCuomwwj#C$8b&Qgq(6 z`Rr96$!7}tkf6;+@5orLe3e1?PVXQa-c94}x5yC_Z;P*6(UGml{?;1)j#=^8V&IsJ zP6f}Is;A-hJq0)?BUdZQkD!khBKci15j_sm$1-IHfo*aC{!BrD-uo-+kf*jn0UGCaR zQRcnliFloCR#ygtx68xe-L|Rvgj^Z?-}I+=`MbcJE^n7O{WQ~07JV#6*0z^o|NC+} zDu}C;QrqxlorjaQcJV&A(p+|;gD>)BY}@TWW82oFOP2iJvT8Lidy$uSpC61b`HJ=j zo?(0m(UFPSVgtN#4s8yGM$+27(SfIBa8Z0KG}1!*S{D~BrNc1YeU>$Z7HBEs6XaZN zD2l(@zbHUr=cp*MwVe7kyTS7$^z?PTgYo;IkL$C7@hV@Rj?IQX`a&Ol;Gx_U8oB%g zH1ZJn6X`UfeiC#-{|Pz)kLugbLrc=jb-l^&xHHSp%QIb|ms=cqQM+pIc=U44ap^X}5i5%ffTzTEqp z&Z#Yn!BLGBk5*an$TW0Bg)Ph9LH~Je@%c_aGvQO&@lDH%G?!23n{%oz3e{+z=QMOT zwnD7FhCdb&Qioc8aP_dzSN;M0KA-ldAP14V zih(uX27EkUzW>f$7V$d8)qW2wF`kJnr=8o-8TD$9@72gdV2D!NSw6F6V7oN*O%H6* zW#qLW>%+XO=c2qD<-O)Aa@m2orA9I=tHapG(~9hU9(-#*vZ+JF7+gE~0^k*#iYvJN z+IF+e?;g8(yX@wn2RknhPo}S1t$52^V>c6XPwI;AVE=?C)0Z;_wbgtpa&fLL2Noc& z6Y(OC-~QUo&`+A*KG2DsyYV)~k0zQ}Te3doxLo2v{bVO+c4QD+Ql0*6`ph{e+r*JF z6jwri4(Gbv(X$1_*UbLZF~lNATJhH5(A!|fQ^;K9k-LN(ilDnnmJwg0#zXT+@Jg(y zWjOvd-y7KbumXBW#+aO1iiNy4n7PYi9!En1V~9^3VeQ7mHSA|vX`_wye88lBqVy93 zuJ8lIG@$2W+BCLXX5*j9mlECl7vFhK%uTVje{k^j9C#I;+l|ZK`ib4eyR22 zwYNVu-HLaX@X7PB#+%vDc?o}0mz?<7(yi1PYym$lHNH>_c6ST=TUvOw1=?xh`8}Ii zX990-{eQJ_xP-WzA3qbDUOB{J#;STy5yl(6g!LPP2fL>Gitirg{p4?YUe9`;Nzh4M zhRFpi@zO|Lal=cVF>Ab{kDIWsQqJoJ{(N|@C|iA5@vr=!!*%%VPm{m-{OY%t?DX$_ z;vjLok;s;*TgVHnz@}E5a0b`;%#GssomH6)Pfs*!hQ#4H7qSrEW9*jJy>4LLOFZaL zo>}}$&UB12<{)weTW7n8-;zW2EH%Rm{1!Fh@x8KHn}UZGt1M9-hBeBZ^LQ71%69eA z%+-qZXZEVapANNs|HebAhpjUogTyb3;dgW9BXleIs94po-w~@9-9U%f(t{N5S59Nu zdz=RkT*MI#T;=fdD&Pt^aA{A_5a80fSs2`kzP45#sbkSUQ6Ia(#4A(le?}%*aqQ~2 z_Ea@PQ{pY|96tB^tPjBrPN+PXsG zMf%;nXP$${nrY+D{7y~XXQHCP%V-DMWh^FFtb~}Tn=5t!>n|~bmt?NkFIfZEJ-5#= zv}xj?(BpSmLpY{SU{{|b>C>JUyH>OJZ#SpSd#@9`y1Z&?@jG-J0;dUC2OYckJy7GE z_dv}WZ~m?JotZsbX+HW2qm%44X~h9&GS`y31#^P&Fz_1x9vr)Nj0>aI<(e7mN@ynx zKBMqnYyxvINpjTJaXmZ~0k21(OYPyULYFKcUSY1;Z>+UCpL@-|<5K+&Eoz^DY@x4% z-x4>^l2}9$a9+GAiSr13N13xG=q5ISu|czTzD{Q213VXHZ%k?C{DYChol_$v{Eqz5 zUbTg+FM8+pNpb}fa|>=XUd1j7;>7Y9PlWN9vC-chv>S##BhaR3HO8DPU%_p!X{=on zD9pF~ch$J5bJ`c3D9_N%f%pk@Fq9a13Js0nJ!n<58+qwy-DB{K#wD3;(}Z28pj!3c zS)-oAzS|<_%+ndl8T(C!S)*3GSA4uedY}F8pZZFYhQyB<4()u;Bl{%t?fhArekPhd zW}i&1v6Rwg1!q*G&B;p}!L{fEyohIy$3I{5;MkQfk>7!L=!QI*2Sndy4OsdRe%HK4 zCNsCtqv*^Y`=mn~{iS~p%{>Cm>0O<*l;AV)6y8dk{4Ux1y3x-;o9}tHFLURvg`4~+ z_V$|n$IwIs`uPdtNQ{m7-*lAUktwosHUB^2ceQx|p@mTGmGIx8i$iGSSc34V{d&FZy9$1+dKqwo3SRF1WlE z`E*+_-ZCFsxeEKD20vmkzE>?V-23t8YQ_;imTow+afM>YlfVIU6lLE@j6Ek!>^EuF zUK942>>V?)Vebps4SN~uUiwS$Lm_)hhO@V1BzsFPDGsz;RvhrotyfpK)cYIS)@C;B zeln|J4|KJ63w!jy(G(X)S{Fp_wbYmJ+&G>a&vS}17X{WkbnWW%iR4|s-!C=4U-BTr zJnF21&f&6IvKl&}yDIE-$*GC#BOLn-HShS_*elCq-`LNF^T5+($vuL~%eK#u$RHmq zk9;wA9cqH1SvTJ|16^NC?K(TIme-iYI?Uq2_v%`pkydC$u>$Rfv4U1eI$8TkTJV2V zcYZHAvp4g02Ku#_b1avjr(*oB`fbXQa{u0qPt%TXqju8oRh?@c=mTF=okI)>SzBub zpTGF^AML#F(@z{EHc+V;acxfU_Sc@cZ>O$(Tz6orb=>a@{0W@u{TbMSdhh3-{?X3Y zZuy0I51nk@yZHM1&3i?`_}CGr+g$v(^_-9pb8?@S^klo+o~^^Wq=<;^5DJ{`$c_Ki+4ak03K@^XaSE zH*W!dn|Et`fhT!ae?P%@3w+uNpSO=3-_ZWJzo8v|ZfDLlkC8&~URVTO7D1P+U0zZI zT^0q7JCEX3_xum`IlKDzM*2v|PrqHy<&`Ul_4w1yTU`qsKW@eMJ;{Epr=UN0Gg`qJ z%FE#4I(T?FJX{Z~{v6DGeRPqig7Qr4Ts-|ED?pNKe2P!gHvdOIf+i=dpaLvO|+74 zna|eG1|8?iM zTh|_}!oMtpzVG__lAYr#pE{WJ&Wi`L>e>#jxuazHnxo%bw&rNfHth7;Y@VOM^Ue2V zEq{bHrIYP)vVt*^sZj%i{>*V}?l`oeT00xhWZuVdmN2%@*lVBzWLGn}OJ-jLI!OB>?)L7B7(?tr z@vc7oLj|VBPSSrX;(S-Z;NWY!>LR&tx;8>eT_i7!UY^9)@#rn%j^Y#2Tg4rjA4+et z?ik-~ddq^|hCy#HLT{^}w=vL^=&3J9juj%0h9j3pS|)E> za25Iv{i^(X!3QeH|H~x*Zz%bHj{su@JR0NKDDQOgd`vn8St!1VLCaC>j3#Kg4B4c; zbdi`Z^x`Y=x^k|Im3MQA6%Sv=bC)1L@mI%_KTNHJ2VSB!_!?@1!~64^t-DSBu<|s- zlUjF^E$#YavN001a2#4X8TvCenCc#Pv3AW{>|Zo`Q1#EcP`hR{F=y#Y{WJEhQ@>`S zIiK*<)~}f%9{?SqIykCLqukOW+8^=t#7q9%K75!<8^*3xHBxh=4lI^h%FXeA0y8ry{^o+$?Gy}N)=YTKt3G0 zR=!jOT`M00U5gJlAhh?7M%QkiVe8uM>epxM+I3ECjLR=3Z)u31b$IDoXwK+b=+5YF z#%XjeV~v(_Px_KR(WAs(sZmI5E+3jS{uz0%W^a&*wb?atgoE$UccjYb`t5V+Uv+$9 z@J1G4+caeJWBn~`I75$4{N!HaGbGOocIzUlw&C79=xH#%)ogq$ zaH!bIht*2_4rhV<0{YYW6fO;#+DTkT$jgc1SKWt?xVr*>Yj%)6luIa$m7*7BV-<5wN5=>O8eBFn!m%v@CyGxEk~6t7vxem&(K{*Jo7f+HbU zyRjGlUvBis)-KkPpX~S|3Hj=`>w`6%B_DWDBA&AwJG%p$de405xr+I%WDY9)f!J(+ zpsCUyXrAj2wA|_swBF_qw9WTZW5gfW?9`gkcuMgFS=X3al5g#h&-^kzNvV80yKU; zYq6)CJ*n`n;%4%n6}wkFR`DRM7ie85!N7+bmT)vxOWV(csDO)E3S`PuMG6E zJjHxG2`-kwQ>*dsmt+6c0snpy{{3Y9`zcw01+z5Ap0gM7t$A(o zM>6q4h`Gow4MR_3xzC>esgWnqk%M>#zdj%OgIAuBKl=joDt+MkJ|Ak2mpHz$+LQn1 zw1=HzYP_y>+Uv3(<84pzZl}F)FYJ44gS*Jp^#y00pPyfL{z~NZa?aG6@p#F5x`q#@ z`mFCiOl*rYM9*xntQ#MGso3S4ks-w^WX~DCS#*}eH*P%5-`Lb*`&{L=kGfrTO@)gt z)irhCp04^w%f^}aghO4=th29OUnAjbI`62D?&3%OW)PV-HfYT&BhGe=zsXpS@i!U! z3dxL|5uugF-%O5ujWhP{!eQu0{-%eH#H%BELPz9Axa}dcpRtHnsLerko9i6fT<6f{ zx)b_}URsj>=F-w8}yz^~+Mc&{4$ZD}Xm(w9XjWsMalSp)yrcdDFB7+LXlJ`y!{JfJzLlE(T7OrK z!sZgr6JzX;rk_JK;n>>t@;U8usIt)E&N)=vlYeVsE}pUfsmR51%Gmc?-*oj{a_omV zlMLSJ+1RlG7C7@9>8#(WwZ}P(f7sTzGvDkv9b*~2r8VG>V!j_f(U^bp#n6nj`F`P4 zj9E60JLan3dUwn@i7^ienf2_^(D-E^&6sDLXv{fdj~Vk1PsNzA$GrSpl;|IO3Y$i2 z_sS9ZDCYNqL|l<1pZ$fs+#;$k#v3kcJQ!NVlWA%pbDdz$`BOPq}L{1zC zpDLLbVc$!Stt}(h%M)8$R{(#c#nudeoMrYYchUdB$1(m7K92UjY0tRgKZ+rGYdR-r zpSaq!zfJf*_&S{Z;fbr^{}6xsV(8tSN&knq%l?G_Ga8xZ#I-MX##M4ie!bS}=g{X{Y4He? zUuW_!lkF86Iv_tC|2l##>}Nf9D4!b3|7r#C)Dre8`N=&S%BP;(j!pPOVdSCWQkmpl z58>m+7E8$Y-pE;~G2~2He_}h>cz5wl47~&2a_S6xriFY0JXfunWjr@zBQXV@o5?q8 zX|ER3=PGhGhx|4en8~Nu$~5;fbnj*E@p12!6AKvf8u$3P_cjwhDA*nhl=CT|y#m@( z9Kp6D-5P~9o+P`3tMfEJ;j2cX!J{<--TCj;Mq(qDJr zd;LS;d;J68>uu;axnG>P|AY1ziw&*1bMPCdje}|QKM}g|(!Yyu#f2ol%9R5L9m$_4 zz1cc&t>R+TRmhxEaj1~bFzPh+;v@Pl0+#4N_O&oitIS+wHr~kCVgsRn-d*ut_Grz6 z+6h1F?j=v1iw7JyPgKt`eZ1_2b>c~>sGG4_Cocz4L{931sp?c2pK9eg_{E^3ix!!V4B-E8z1VAJW(5PG2o)^l>uu|99P_ z|GpnW|9u_UP8EM1c)WZ3+064FBtM$jODp-74&Q+-cD~`g$o=>GCG*>s!VlYXtY`Mr zV*A{Wte-1el=w7pktXb+W^BzCY|qxhOzJviQr9Vyx=xwYb;=Bcsdw@zXlMUR*EhVq z>q`yq{%LZ<{t4L)Z%@f-cy~HAlNa+F`a6t2c!M9lrDj321bYMDSNW?tE3kz+$gR{t zZlexzyXqh>D-Lw9ZnB3M-QH3=f9|c>R*_I`Ce&V*@IPv~&tW!NK+;nlB>M3Y% zKt?eBPAnr-pE01r_rIO&Q?o*YGAvUg!7bK1CGZR2|7|0Hp(97BWO51`-v;5Q8ZM7yHleToO)YQu-{s~_Kmy^Ne4XO0YdaLMnKe!wKop+sn`$E5G zrqHjQ)7jsg=jo+mV_Oae)|eG&IyR?jl8;c^7wX>o2Q(>J>*30eW91JU-p(d zG(4F3jo}lVJ&t$A^G*@(6!A_G?-cP)5${~x@JkoB4jpgu;8ry5+83^T7;zl=@Q)`c zAO3&IhVIzw|LWp%3%2R9l7{Bx;~H8kiUMu3!Dl7-oC`j01)rm@ZunIiE@yaTf_G1k zS03DS9C@%Ge9Hfi!25fV1^b{&#Q*J;Sx>A|?ppVj%^s>SD( z2jRuOhUnSAxdmFS^OF;t+0a%2tKL0=i-p_8MZ5#8g5RdP65bid zJKz@l?gqad;CB!B-8<&$2JyO)xsDu&cxd%$esg867rzsagI}Z1dLVBt66{YSL^WFCAKLf|i@1 z;TGg@YXv%t_3d`nw|BF?-NE{HSqA5eV;h>ihFi!_N4F~~*w-^SLGC;d2i(oYrz@?UeuZ`WJ8&mF(3m%EYc@GURhba7qww=O;^e6}sb zo*mZ~deMWAdksF4`TxR4f6)u{u^0N-2YtLHJq=!>h0qPM%G5nZ$F@v^ZlI;MmpCtD zTLx!qW^lGU+ z)MghiF&i(NJan>!-@NkbuSqX^{@cZeO)pkDKDt3KJHF+SGpCSV4mj)ky@_L{ukVX) z)}6uH<1*-^4t=v6I;ls#tcFh3LMM-7$34kDgQv&~dlp`M0bbhxjrn^8I;+@w1ASTS zOVj?Fcj4i8YBE!DubRkLQ@nED7UTCJ1q-@$F}h2Uwr?UO`C zGhW7>;*Y?`t>C&1K5hrsyTSEWzVFhlowM>47YFDB#qK_%ab^=gaK}>UH)C0OBsmtH zF=y<9lZ3ytC-C2%3jdHa_}~9#*Ws7ExYy|41A5sju?Kuyv%1`Wue0g3tQ7r-a zid~d1vbAkH(U!XlJa+i=&io61uNc}*#ou;lSN?Pcwuq0|lwwkxN4T`Y8XhX|lToX^ zhMhXkkA37rt~yd@pFhJs^1;w@9&2{&J1=H^KUxURM?%-bxv!jqKWBs@XRw~Pg*}t( zF;_jOFSfh+Wj3B3cJbuU^A8+)_(rKzn<~jYvR-ACe{Z;4@=qSQO(>k9_wna}T~!FhuN=U-en z3vIj?v3Jw81K#AfbbZ%u;)AL1?$;T2=yV_Ont0#`!M3PhC=yPE?fNv>MmVs=(qM~t zU~BTg=E^tGN5gU8TL*lu&GCmk&U4HC;d(F5$eCl!=-ki?X>bisgUj#0)tm-b0sCQH zoTbM%?e(9$l=)r9evK96bPHFuFIHscC^xOY*6Frv+3WUKVcBy2{bYap96Y%8>+!~~ z`O)~Zn4ezsnGKILTjs8)*hdKp*;2X<26?G7%5_dMWv@Y*Y?H=UfyEUoxdt1pzl+J$_ zz4Pej(Cf(1Qr?XsBc|#dzi`{K71^d-AOwRzu3gK4w#T)2d1cvjD(_}d2G{>GPQf=A<%`x{^6@B5B}>)9S$ zcO{-?^ox-vss76i_%H3Fv8&K2t`B2!K8Q(0iAhnDGeAwwKr=C^mO6CIa&$~R`sEVF zP=hX6j4r7~m)u_zIQT1HhojfK9k*w5f==y)ol zLAn2;p#ctEM@oXchmM#;9APrGVy6UoFUWgA-V5?x5$_fA9%o~^v{vEh2amt<3ctDZ zZ_Cq{D1jlJpNz(d(X!|;A1@gPi*M< z_;+yTg?yfmBmaNr6pTOgF&_ULr(pbF{uqz{QyY6u{{ue8<&W|B|MGOt>3_h-c>J4BLHu*a zS3kb||M01Z|9y)Es zE3o@*OL^wxPh>T`MGlqL1NwUL;pRKUSO;ij9iW4CfVYCY7v#Nhyf>crg7HNDQp{ct z;8d{08k{$O>938C2iEo+AFoXH$cK+RJ{I*HA9wwg@v-zHqI?;1|)yue*th)15vV@9a5!{GV>( ztlhc_#%71k7_z->k<;#Ct^5cQ~PaYqx{P@>y z;^cIukMHz=Uql~Y>n1Kvclq(?(w@`DfNtXCbf%Ag>H)uqKHkvz^=>ZE-+=wGHPmzZ zxVM`)Ii2ZaNe}o%^l^1JadEoKk3T%nbNcXg6DOxLeKhufUqm1OKKXA;engk`oIY;v zCQeRg`uKGZ_(k+Fs++ht-Q~x=2YXH*2foxjo;jWAV`~riMfCBDZsOu}r;iJI5MOuw zmz%nYlhc_#-mgA+ei40K&`n&N?)34)nv=(eOCOQz|90fZCwjmyqK}8WiHp;nK0b3# z&*|gq-Nec1EI%IV!TOx&A=35CJ~`KB&Yo@E{I6>=Uj zXEhJ$U;oexoHt%R*cZ3>docBo%1cU~-)vczzs)*4HUHw~_q?JwHJ8laWqD-NcV;wn zQm?1XoI}JJ4g3~f&37)}tXHd#0;la6g~_&cp7JKz{1#a8JeR0k%d{A(> z&+bS*yM4yA68G8dWz+ml?Ie3|f#lN=aP6%JcQ5D7zuzxek1W48XC)8k49)X7FT8qC z=sVQw^I71+>Tk}()L3T>E=r6!Har;ToRqjZPl@_&t2vLQqWYZB9l!0YbKCeI1$ce3qvp{p5>7g5iWGp0<9s*J|}xWqj- z$KJEC@fuquK1_bOTYt^1|2g85$JYPssAvDlYHCm|r^YAsKg~JNsz*g#s_^~n8L7#n z&J?vaYpJz)KeaZOQERg5xP>#IpD<@Y=Qcvy>qOi38PGZ#w-4tF_HE2< zI9k_UN8QQg)bgY@rRqj?82A|n^(vYB?K8poSk4kDFwEPK<6gUfZmdkQipUrxpC&)q#7&k`y_B~w?aA>U1{AFsr zsb7Z{P5;no9KJ}LNuCW~41qs&=2*Cb+JwNXn&Qz)KEM{8&o|@L*r8T; z0S#P78tgNMR+ux%IRn~Li`2!LaPZ2%9j)u^%xUQC?A_2g&5Dm3Vl7>ST*(7Z1=Qlc z;M`!tg+q&|RhrY#wzhY}1)K?_+P*FI*Kj?NTJ_WnAD-2q+Py8vrKq2Jlhirum0?}} z=5^LQ4kMlV=271~;rz{`{&_{5BhoaITCCI;6YqrU*k8n)JzD^sdSJZkTx;nX>M}Nu zrA{pEUHvJ}VWg(xi+!?cn@bogG;3|VD6|q9rdB338iS@r<1b&KMx&=4opn|0oGE=3 zILR}0rk#CC-KbmUcWAylb$hPz^2ZkYJn5Uv`DX(XH32)|i9PVg-qqAoUQ0dYLcYP{ z-bnz`^w^D=P18s1n7Z#>S~2TyYL!Bd%mtc$C6Hv6)cY_Kv9=zLhU zskWaqeA73LZ~DSFXTmrA8o%(^(Yg-tOg*)8*<1BiA-v=A4Ra(qon`nY>jU_vZ{t%B zIDEr9>FwEk(>KXCC8>OK8??KHnQQH$w|IBOr5+BjB6@^x zpw}pT7zH-NKaA6y$;ViAu7u&6>M@}RaD6(3Z`?D-CLdB=R6{$QU8M7J?7_X6l2RI8@sfuSV7Di!z>qN7B!IvtbXz2NCK% z+Pc8Gm+kODH}>llP3XQo9CY|VxM;MJ;+x$bJlx)O~+jLfjKccVmLGmKkP>4 zwa*5prQ!#FAezm2Ho2Uw(2w)h2Xfx}*_^jNm>TtY{x0c?>b2;K+31Q&=pMYM=n8Ns z8k>o3J+7`m*Sfmme?kt;^GoBU z3ogxjbbY$6xPv^81kGOtEs5sQtnJGy+bo^IV(5Z6hgz|8woh%ivX4duXqV zwIgRidxqX>9Qk{m=?8BY!KAzjKklw`K-1k-ts8%q8{6-x%c3_rS5Iy z-ZErO{oh*$M(Ex*tph8$_gkON5VQ_7W!FE%IdFsO=imbju%^z&j93UFTRe zp;z4J7Wu8wPg3jlesqv)w`KE;t9u>>2a=;Y3o(o=)46c2OtX0`vvEe_(K@$Yuk3(C zy>;-_vI<$pxLy3Y?Z#+lo8%zBncD0;EBoNbFQn4%O3n?TxL|*F*XmbuHbFBK(G5SczY~XuF zV}M5^quqMIUoyJp9e=8h`~^I2jz zea=~uvW=G(WR5oeMg+O!p3_-|on~3*hkkQ$(%&%kWxJuD{&l=ZMsy*kTJgL??Uf)`^BR9J>PTIz1bTv2gcs4b zc*oezk56piZ1IN2F7z4O`O*GXyaKtpDAU>z<*ab=(Dm5gO~|7d^D&G1<~Ks8v!T;D zoI5#@vlG2`do$;6v@n;g%wrq#IM(XjQ4aswc1m!)YuD=>@_PhV1#m%s?S}s{8i)1n zUAt!gox9eouy$lwS+(*-ZY(Kz{?WsomxrgLf1&vZywF^l9H;n&b3a1c(NW>)j1d}k z+ifnTZE&mo_%Z0d33_UF`qFvuwjJV}Nvm^D`Y}#?{1KWzr^d9`zf^r=$s_OigsR_u zSASR6B+q(u+d9kBmiIi>aG@256{X-ka(4Ygl4b6>qq6-( z7w$i|-mqN{Jz0-F-R5h8FPrMF#&?>~&~!HEE8#0OO#m0rOLz-s=j6d-@V@+@|NVqJ z4!gFz4Wq9i##txAOKco?0mevf5%-E4j#r=kUK1}$!jp%b$KQwlE(nc$hB=~N`})Gr za9yJd?CXm{g}N?uuRjyY(>3@FD+Xj;7#hqqG_ll)+gzbD!(~&%fWL`xh^}uW-$VBc z{NR?~6jRdgh2UDx3zxG@!?$75O+M%XTVjO3NGCY0EU@jtO+vgmQQ@i}F zcEf{tA76PkYh~fV@WHq3@A*gUZ=0@c913qg0dL;3yflaI{Mq!BJm2&eoti5j2w znV)K75pBHfv0>f!CmyGM{|(gd4^qFMvj`ge{finRx%M3T@S7vR9w`Y1R%&bmRqOm% zTJy$g?p~3!l>|q*XdpWn4+D!iFQL$>hwZ72A93s*QxD(3mwMjQI^bIYe3^rS4ZY4S zf-dph^LjO8>daqcP1D)qxxPAaK8a+6Y=-b^Xb*hLe$<>!O33g@^cg(Jx zzsXapzr*R<+y98;^nWS+=hMHReq9)JPVjB-r@}Be4Tk?MvFEBi4ThB-7zCfU|Bt$U z`C0IYOifJ(LeY=Rhcy}jQr}vp3(Y^ zdtWv{)i{2`hpU=t>VHML4%Yw&)Pf^HyBRz5X%RVPvj!Z4_DW)kR}IZ!ImW7#G*~ zjM?^gdF8m2XN;diyRtdG?~1OZzt%grdCenxQ{-PC*>_2OmBrZBFt!cA9Rz;U{v>P* zY}_!qDk~qG5}Q>1p6s;>Y~m^_tK-|8LDq}ks;pie<@_DH#ESPiSGKc%+txnW+g?JK zw9$4B{&3!SYu*a%?7SJ)ya;~#aCFRt{GO9#g|_nd2;~0NRfxdFmmR5Vp2WF5IVP6B)Y#9ghn+)Cj@H^9rL7CV#R}|~=w$RL zenB%fa|?EI8#rs1AB?`$`63tMCuL_@J1)3=_@P(9SKch*M{kv^EGWB zm*>6h{X5TlpF2nM>a^>9PG@l1^JMFA={21PvjTWUH|7jW*~rXYiRR9+%O5<^{25>A z*!fdi8=yh&9A=%v*qKAkQwwvbxs?sM796!&8MTl7eK3&i%dTz3A8PaGY_phKwG*{+ zmgF(7n>iy!et49*UCUgyLMQsYQ?w4Bbixmc!+7UYbE$d6_l1^@KZivpn8UT?MjSJT znm_Q`!W<^%kGb^D-w@_+8~Eykh6|WKoxL+o^X5FKn6%r5&Z5-sKR?0mTKi0yFOT2! zr=sNi{PYC#Gtu}@Nqe_iG3j7lD1Rui1rzg2+Pp8(-a)6GguQFlE|TY~2;ce~`2uUq zTfQ48=1h7!hQnG6_Z9`mhNj;UTvl`R$URm5jLK&GDxG1K{><{DrRH~R{;4%b2j8>2 zv-24}OMGQ?n074M@pFz_`g`8~XVE{;k80-mHDz|ctDSyZXsb82I~upPuRE z_Q(6JHy){T$B=mM^VYiZ%O)r`x2|IJ1QSPd$A2UJKWbT{dRf&EthHL6Yei=CqEB;8 zf4W{vUoSAWD1Mdv&Iz1LWX4B-5%_tP#whz58SM70_r)7#y%Yc88{aQG6!gmu9{UIR zKjMqyy_3gTNVS|5C0NT#Cd8wKmJRoW3Go>1B-+FuOKB6?Z`#DhkI}vt4sVy!Jc&*gf~SSxs2H4Nfun`sM}IGL@T0ced5;kD z`=-wDGxTZOt&&H1(C0VO==0t)r9YDXuXp`LzIZdbqXpeD-Kq|JiWo`>cq|TDA>W`N z_{eB11E<)ueA*4>vqn5N7~gO%{L63lIGq>({<_)`yv1dlL&tm+ z_wm=NEy25G}$b=pij{><0Wq351q$&#ozM#&3I{7XEK&*y!@^4{(2g8=+D`~xM-%>j~^&{sqlw_;z9I};zY7Pq&s6gHxrr? z4i_DL^W~`j2kWQtTU%u?-uttxInCv~OY>}Sy}hRAWBpF&u;n!t{hc*xIe1(QO@Q<5 zbKsR6WLg`)wG*Qqd}nZ3yOC)*mAf68=KglX$h4eF=4fiWBh&OuGxI5%rD>8Cuch5r zk#QX*#0jT?2i7m2;LI|u9SZKINze{+Dtn}Lnsl1oZwK@DDr0FRmVccu7`WN8M(^P_ z@$P;7t~&$%v$lO?!?KpSR{XFDc`1Bn{7>i8!r6G}{BGzkd+3V~ss9bodMk9^ zYUn(>_9ft-{Z8l8ZNyP?-p?K-9m$+6Ev`O0Bzv-B3jA7;5i;wHs*HK$f+L%RCg7H9rqCoBHe zN}pPf41<$!H~LJG4P_UFp1I&iUCO-`7lqbwZ;Y=x&;fnV2DVo4)q$N{c!CUtkC`&h#;A&dvwkKaM|K=l(8S*z(H8gRzSxm(I3j(j8+xGU;sadYttH z#pCRFVbcCG@h!y*vx9NTPw|ZOBl>St26XSYqR&-e3yW6FH9Vthzr!;X=tHU65_>BG=48D-PL%^ROnONh< z#7Xig7;B15B+e$8D4is|YIp~}iy#9<7lM^Dl^YkJmkQz6!mLBjil)ZFw^>8CJXGcA zC+Vg|=n=`!Cg|)gcy<^(EB#l2AJ>e`KZEv+Kj_e!cl`5d^KNM2x6b=_LkrS#qSG2^ z;VzAZ_q8@B-%)EVQPwqOdkaQyTYpz__!xQ{1ifi&UjOLG7abbt7mQ=SCvYnqyY}-4 zW1o2PIg!hq^;ST0pMf0J{8upleq_GjYJwK$V?U2GGT(3PXW6mtZ_|y;_Zyi%&XM`D zn^oUg?a6-D8lgGs79I1snZPJ{f;^Xh4-U*Zxmv%H9~C7RAPP)|KY%NP`6h;cMM0Q% zpn=Xpi*IBg{#G&mR_RpRr<3lAzzaU+Jc8aAZdN$Qh>{$Mgp~1N1 zVg+_Z1vDkNMCaE}ux&?O`h-ZjG%qq z%^zgLA$gO3wEiC9`7mI|6}RDtNdMn4Wsjf$70upp^uIJrHvR&ZxD1G$tL ze4c!d=c}xY${E;=89CL@7xK|HdTMG$AAaX!e%Cd;S14Ib-w|vR3tlhagYJCLJ0|gR z%C&>%VGf@E=;0|B-y;mX=OuDE3W1e#V*@po|J4fMT@AcCQ+6nydi){Txqe_#u8OW# z0-tDXD4!}PFH~^kK!;in8N$boiDWizyC+^J8!riGa=lUWapVg&|Nkd&ei*)`8`V}Z zzFTR~I)E;n>i>TEp`m;PR}eX+el$;_-}L$Vu=f9d;HTz8@bgqR@Z-Y&*QCd3-%I7O z|Vy?pHqHi)q})I;g_oCoLF)j@lxUo@8TQTd~5sWFQ#30cKXPPWIsWt zzaiWY{V|X}23h-yFT5-iMQ1D|Cq()wKVP`|LMT#UgSi*38}o#o8;Q)%+&z!twb&-&g^kng@FYsb$Hk8P;WutLhw z32$JWTNvj{*oWKjy?%#py~~OZ>tj6~+ah0!dmFGv=aci1>94IgT2fnnv}jqh($-nn zxA@-Y8@g%#H2O@q!2XZT=bhR3^_kT>l?&R6|Fh$Zo*b$Y@>e%wx6Y;i>GZvecPi9B z@7?y{eZArB>o(r0NbT!a-oCc@LZ6_ocVhmO9QcLMqi|5-<81mGE52nA^SAN5P^=1g zrUOqc-}p+g#e8Q6*TTcW!NMR&H(OCTmC4d;SC05py3w8#1%0ilkFg);72@}K zzTO={e2dB_PEzL8v&(!SOKi?x>x))^h`^jkaUqARbZjdlmFKhaiaCwrbc z+4DqfM)@j{kygC;lh*d)E6)naX59=g$Y!;S-;Mkz^o4%&vqXLx`DBG5TV~j69LH-f zgS{HX-}|g-Z;1cc_8#kIdq%$XImfiON-`)7z6U>`Jy%AKV~mTg`+P_~W)ov_fKU?yG@pC-7!t4J{H13Yl87o%hypj$(oukJJ+&0YOb}WPOyC2+K>tDtm*YWdhe2O6>(9k zcWoy*$Saxu&dywTy$_$h4a)bARuFp-f4&?DnS9iN*g};aevf5@HvTllUcjeKw-=oK z4)i6sw5H(7K*@u#Io5$%V9v&O=)|rO9NC%n-<$9mhUZv2cKfrpUHe|uvTJ=+51e70 zQLA^#kcHCCtgj9-ILH2S=P1VdViU11a*7^IodX+|R&>H{bc2sLkNgeqb!OUidkOKu z(b9Kz%r(Zlt*?2l@un|YQzzXt^U5mZ zZdn01=C{W)H-2Levay>w_I7; zZq3_t8SgPi)}_|e-OOL9&pL4LRo2wkvx0#)kl7n}W(It`VC11+?FL_a{by`@0(?E; z3qJ6K@66gx@MT>jeC0N7hu-#*C$4emx&waG8tGbUNoXwJ2LCgxL(k5lA8E9_$X(#hI?VQBbf;GN8M zzpC}oDeP-OSGQuPMen}`dCgwB`fHKhx%}OSzxyU-H#DBz#r~R?k85aNp4H&eqw>^O z^x61d&BQ)d@+~>0k4v8mu-A`ApD)eHzj6)uA4}ZqI_C9J%fD>{b29`!*+`tH1=@LO z&a5l*xVIWV;P05HVLY>j-}Kxy@QvPmjCY&;cdUODzHh+>x!~yCOImq%BX-D2o?mdd z^J(Q*wv8rVxFi@CZP>a0S&jcpZjq78ykBGfzGMCE$~Q7`x;xhY6MvI0jm*q#6ixrA zt<%u?&DbROofq6yKg^mp2l}rakX73s_-?+-XW##%HSaOmG2im-sN}gyeiskkoR$1N zr=@f1&Hrt;qxb94Pkk8sp3eH6RlYuz6N#m|a|4T`}%a>5(H-k5a`%gWKfv_8FsJ){f3=H!R}#Oh zDzXmj0q56RIoq%cu_LVm*A7@noH(~*&e_(~jE-kD-D}&NqxURn zhsH8}y;&>Oz7pl>oPlio;rh?M*I5_zS0U>lIP+m}^3(I|`8Wxj^zR~0T$!(Zhq6(g21a*Yj4kv{Y$0fHvuq(~VjuElcNgW$ zTMu=Q2FpbUSCV@`e?zj8{dHBI?doQKLuB)xGt~4K_0Ko*Q{(Guf6sQYzuiVJPXEkFE~*;v-sdN9Iz zL{u>z){eaICH!IDl|Q`F8JqEUcyduioEOMtHB3$`*HWZ2zJeD0eCRuW-ovG8p5fD5BZ#cZ-#?E!Qeak z{as^^`VYAM8<_gs@xa(>e+o>Z=L0qN{^AC+C+4ig8bK7FLU_E<&_}P1Fm|)KHuTY} zL%vHjIsX?xAFa^Gn^ty*4_oHQQ$CaLbfne(j&Ut}dky;j4s3keS5%BLukpHnbJvWn zH9T?T2)c}1sUu^YHg0v=;J(^;5&0QQYhz4W8=cE?4s~Kj#)fB@cbfYJ*I&&Vinrg- zq`k9o5HZ!Z^Fl?$p=_SA;W0b~O^KggA8pHuDC?HwlbUtQuE+xyf69|e!5?#%kAC&y zZzXeh8RPTv_1d)Sfr<4XTaLKvF3G)O&i%Cb*w0+}o%o03feZh{BFVGldTSJY9Rr6E zWZDJLa|`nAC3v}Yn)ST+L7&d0zIlFVAZJAN&fa%s4<2#nA+VEd>tPE-=Z}5)L+MOV7wR@E9YE%rDUa^ znE*_!cU-rtH7C1vYi9Pgjo?yvT6H;fL+}Y-0*=;e#)R7E2v5B_1V?)Uj$X&$sT?@m zcMJ}ILH!6v!c~j>MR2u^IoOJCa?_j(Xg9YbgLZA)=N{AM^=WOYJ+-CUFfQ(IWL&Sn zgKF;rN=B;MxOy>}AY?%}X2K0lr)2eCEpf5yWGmhwBXt*X{ZRFuVaRya6q|(fiEWC$i4m zmVoQR?!o2n0bJJsk4ukRGy5dzppe)zxan}_zdVHwkDdQ0d4I>li)b;O23~V$pwiGl zAH#z-4X7SaD*wH9f_8=DJsvu^8$8=IaE+mXZO}l6hX$$=G?1G@14GWWrrwvJfj((8 za4bBJ0gvRtamVV=fZ+1dz>ggos7la)VEAAf7=n+~9k>?u0Io@Q<{J5=b%Cv!xd|HB z%=#F3?J^C_IKf=?lm;f9pxqPF0CV5s%>A3_ge~~MId}FNoCEJ$4)3pGzr#I@&6aOD zY4T0`LsIGRBH@?$b?3d^k#E{N;GOGSLx&@coNZ$r=1ufK34A}1{zow9Bk8{qzL!1_ zzDH!`Zp(LI+KfG;wVbZPl+{g`t_Fs!$o#E2y=pJW?3J8nc-u$6Er!>9X7726BL~y* z&_@1L3uAZ3JQP`e#qvU|-`>^wU$oc*s=ldP!`=Il^-}&C> zeD86-cRSy0&UXvn()kG=qt4!Iea{t+P2|`VdmKN?a{hVk%Mq+|z27g@zSNqlVt#j@ zODxRMdrhn>M>c_OrP*Hf?qX{?61D*!UM7E!)+OO>5TU zxYtViij6d}j{FpH5o5>Ux3$t&Cvb;Z(d;TJdPbvxVno@^D(ny%}!xsx8H?UIm=ewUN=Rj}zn89*7vQxcK-7`e1z{ zZfqy;q51+Qj}-e&aSzM#5#ON(0q?vymh~EJ13eR-;QEjrd)Kq3+COc6?{nL6WOlS#IW}cca!&TUa|_&4D<7Z*srSXly^u zy(aD{=Qs@RqVriJsPF|^OML+wE^82W68dc9m8`pl+OQ`z&n>is>hm z`F1yJ0HW8VJ>}}?s`cZ{9=G#D@nOt+X;Gl9LUrDfax2U{MZkM>elY$D@&9X~({Ili z@%`S6O*mL}mewb78uP)^2)}i?>}c7}#o(r8_OZ6J+uwiFU(4(w#%1^EwVSM6&az1+iyBF!pDBVVcQ;RhmS{o0o$O|dcGIXRZ@a1I6TDJ(@jw{rjC~r|ap5 z*{f;xq&j-~0iKZzPx$|7^-!YltryRVOK9!ux8&n3q9*F6=`%W#F$@Qvh4!-*Mu+-4 zs-bO*xKQEIvRzGD10t5l+S$|~eC+0Gts)k5=TK~w;mE;}!T4&fP2O5zFn$(od!OBm z&-Y!=vs<`!pVb~o@3X{a*3b4l8{^u2wwbw63{`QY2iVV_x|VnKA;rzS@t!ZcI3wQk zE9lsZuXb!qZ!9$e4mK0Z$-9Ht)feo2ehbN2R8H@k;UBj@AN>vW^p|;p{;tQC z*Blh~r@yzYP{ly2p@8`)4q7`5{nEbnM#Z+2n7(7oS*&E7{Wo(c`qM|e{@}BR>!Q*h z_$kV5H2b8O$M3|EKg?wraL#bxRG#xA$c#T;Wy_11;AJRyDL(<6`KEUF=R#Gh;&m%o z$Mn$-_3qXcl-aejBlKCVyz2bEDS3UaZV;`Uo_Qu8==Svs+50q`kNQ#n!o4kHiw;Sy>s{q} zy~$ct7+Ni79K;EBWLlZ87Fb!YX5pJW%D$@M{aLdlZ*Zai(6ejuH=BJ{Ti6>}V-+re zj}NTk?;^{$WQOHyWM76^U-9;%w$yeZwu3>JLTmbD)~;bsSXO7})5@!)9!KDAYE%|%L|@}awy5p`d+);N?+CI>@-_xtH=Rvw zP3R~&K6i~GxZWPWecduR7~h(2>xviY=XmW5JJR{I_F2zlKh8?_ovdPy$%-I#PS{g? z*y!@C#%gQqA6C4VJ^GmbJUXz{#T_($!un=u`aBHYrPGJ}oV``-?W%D0^8K8e{MC8z zlr|d`q2((g%uN(qM*Fm40qh*E3y?j!Cs`+6)qZswC#m~m zE_2}3{uq}Z)9Y=G%MHetYp$}XzsdKb$fwQVaRKm3)+q1iPUa}3efkwG9k~=*;ywAv zYX3lqz5dsZkNbYVvlBcY!|n`E1`kuvTNUvCY}R2#k0p#_gB55UOYIhiW^(91hyA4O z>|gvEV{T{Oc7wk*<7knyQL@?c*2+QDmjTeKkc_h z-7-M-THg-z)zrPb|4HVitS>l*4|S%%kZI(^bQRyoiap4Ry`od*NpeVS_NAX2E$bQ4 zf_#9@@O{}z>>%=!4r4O~;q{*O@x-C*QDp`nInMf{@X_DV6*fL3OHPC@RlcT`|B6=xDRcu~*syY82u zE5vi&ItjDV>Lko^>Lko^_I%%kEfwTF+0kc-=b$TF9@_JKs@m@Ss@hKFsUUSDGaQ(; zPkA+R!Vgc6vtrLJA~r3(V6K5#*V>aTIk5cPxpzyBik_h&O! z?e{nL3@$TjXF9li1evQjwrL=<@h9L@&prw7Ep+hUotyE@%^J_#j5qY;uO089r)u64 zJuNhIlS)tNvf-;|9jOz}?`G^<`&hNA6|@x{{U&x}LFR&owEmyV{#DnGlr5Li1~nYU z(MHO>@I|4q+!Owc9iNanw!9h2`7^2V27TwmdR%#9>KA;3^?$(8I?KuqeZ4-ex!W(B zXa=$>ij2_u#&Ysd^tWOnVdSLjpy=7ig9*^gB7hH)_6a#{tqxl7}praAba>Ojk7RyoGG-R z`ahra&_ddNo0Xq&Xu*m3$zP~;^p0pj|BU`|)^A>6&q4b7P3L^(Xfd+C7G9rAT?5ts zp{BpqbeigP&1bY-2gqCVBC0w-i_>aeRAeS}ka$~q-_!x(xtT#*Cb)Hg{(;|2Js#vj zTdiFWs2%+mSq6PnVq4Z>yCHu%D#isms88~~Y3tZJLB8{ob%G>EPD!00bf9eET4cei zsu45+omc^$u+biw0?v_hF~($KOz1UZb5>w;GUj$j|u zl|TnP7twS49^rSFF7nOZ#$KVT%-&_Yrci{MG`^q}(0ZrpUbr%80Wztzl78pfe%kG$ zSg+%HIDD#mrpAzJ3sE0Kd>K(~Az(eGwvgs{68e`p*ShRlx3&;?ez131Z6WU6&%H41 zE6yt3F*Sweqo@4Tlaq}*x+tKvZ14Jk?EdSp(U)MOze+9B>#)x+_Vs!7KJ4?0JF`b! zhwWAG%k9|2{*vAJY(Z@EDESbdpBk^*s9Jr{WTcdPjPDrQWK3<-@Q1+D?#aS8w98yZ z_MWOIePIK~v&hVHv-P@3+;-md1J>KE;S z_ZEYT;oxEqF@c*sxQJr^ma9!ARx~dqOs?rPKBF&uGK-_a_pM z@zxhfuYV+Ya5D9erXrtgy_u|k^uL@T(bf7WE_}UB{n^z0nI?YCSx$Wl~CwJCLw zzNI=y&{C>j;xjry@uOl#2NYu?rPo2)lx^2Rs>uHEI!KSer-?d9@MZ<|aJ2VV>v)Rk z$S!R@8yUWuIi}W);ta0sY~yD9p^g61Kg>rDFNV&Vfaek9)?(<4+LW)hQZHpVJh#x$ z8S50#nfCrxGu|@jEQ~!IL2t+M;G@Cn3mU5r2KI=axGsU7{Pd~%PV&V|u?HijrjlDv z$?pHfOApKTTPJ_Bi1QZZZ*Cmu(f5w+CqMI1=Fm$||4d)5ewV%XVfB=tr*$7?J*B^J zZkgc?yjXWK z^5Pnu)0kH8wqs=XUhS$auOJ4)8ji)wI z7I@ZLTN&{u0h|5)&w9e=_f`wHTcn@#-Wq&f#g(iT z^5ZfRaiwZkHyo!8+2dKrfowjCiK}lnUSZ@G_k%`ex&ENdzmIW$Eg#Wf7P@MDFYD=M znD1JC9+zRPLv9exvSB;z-!vG^ODaqRTS(*rfm ze*bechUEEZ(JE;AHs~0eHquEf5ZV@ONAQJwd^1l^hq13C)afa(22xj^^*Pq1bY{%% zj?Oyd%BbjVz-QuLK2yhdBXY{JdP)9oFIaJQXa)SJXM*4=GAS7Uz4o#L%&!1}Hy-naD+6m8K6E#8mhF_-Rw;pDFhi|nl zJ1{my-xl9?%)cn@y~E^0Vo#6Kv$B~+3#WX;uTSgV1zbmHE1z2CQ`o~9rkx1=Mq>Dr z^c{ugwSE*s7BtcCU8;GGk99Y?TX~O)MSYGonxXlY7&&0*kzUjb&BEuGT%LtoUV~h| zhkOUw?^)nyST;36S!dB0h$#vGCa#FSebD?3drA z>ovDpN6nEhQ3@`n0UNYG=L+I)rS$9fZ)@gy1N4A=nX0)EzK#74o`vJ)*}^R{WF)rl zaBN>-*?yzAhMv&8v2fY$g3O@ep@C%+zpC zu7|ntc(r?DEA#+=Ydtkwg1?ogb%CtVPqlv){Q5YvDFSYN`8IvJF)+o&1>1kI?ykRw zF&4$vvpDCe8QNJbI(6j(bEe!SS4RA#9U0-QKS`$;oDml-250vE>)?9wv(_zgYPz4O zmc0ASaue63HX%CITT5QKa$)H1F6>w%H)!uWoT*o4#+VnXV2nOcRZxAoS@#tGx$BbbJZvQ6Jcu8lP`wSUIwY|i1d zb_i}a=i&#>`;UCT;?PjuUGe?GL&C9(XW7OvCx-9h?=B}^oWocZ%TL^Q;>LG5abv;f z;!&~Vx51_G5r*$fZXthHAn)>|`%LbFTf5C(r;v@~^4ss1IQ-_=f{#(dQ#AaQH2zB8 zpIn@ad^j6Ag(t%U9T^Uf1rH(T;Tv~u_x!8Thf9ltqeJM$1n(^*RuUPgHtf0m4EWNu zcv1JfYubn5w<15Xo>+!0JM1-o$%StM@A2vZ+V((l|Dj?$R|E5(u)QV#a|L{@oQ3}2 zTIIiLaW$fC+{QszX z8~CcKEARi@+?zK*!Gc8vO+r9Kv}(1=t+u(z1rZ&!9htE+befkN-i%hM(`v*v!GIv~ zC0ChYiZcy}pf|oz)Tumn3KSJ-tVWz^JGFhdH#aXp@C9ls7qIz%fBT%1dvZwt+s-_n zKc7!H=bp3A-fOSD*4k^Wz4qFQ3Di8SxH6p+B7fuV=k0>GXwwmM!u!>UG_Q$mDkVc0pLHu-sc44AsjO1FG!|%bt;{ol^BYDXgK`> z58psf@xE71A;EEu-~d0e-(C4->^=DBkHgdPJMte~=$y~xV{U^t+Ny}(5wFFuJ?*;f zSec8Lq-DC=81A&8cmb0mIb!EXKGykMnehtV_Wj`Adp_8^#5~&V+q&1tjP^fZznvSe zVA@aqW!o>wX#aPM`}hB?|KawVKkVQCcQV?4?oVmo&|jLL4F1WVU5Fop^L=7@oKrvK zJYtBN{hUo7i0yqYI~GT0PQW+q@QYc)gMX9@=OJ`cdwgtOcq#+Fr4D=vmp;%VltX)s zCx><>eZ~xZ4xW#;Sb6|->gL0hT(sbcBK~r(Keo;u5j@DmPT-M!Abo;d0R!gqJ>GlQ zzMgO5gVOoF$4O7*?|&lS@ASU?-HyJ3+_UlDUw_g5SoL!tc{(Z6z5-U|`;8Fq7Qv=o zORPn|e813(KNJd%=g%$Epd7~N#Kb`wyUeFK*|odSQ)GWXejVr5GFAy_rwDsWa##LR zBUe}>_VV>$HrB`Nt5ZJ|pM1RWkej+-uQ0yyPY}VAnU8LA0ybwwmso* zIn1+u{dDH$txmrb4=35Ie=a@!IrKjroC^no;d8Ci?upR93(x-+y`1(JpqH9YK`(Cq z{x|t|_%~d7I5z(Voql-v_jvUG|Dr$d{=iI=l8ViO!`1H;^MCa*jXx@>8x~)b_Kvx=<&3Af+b^bY8>!5#s0c zd?@`=_Qf>z@#R>d+B$rTKUIwWTUM=fF+28Nu}o$^pUF*;eHwcETcKLz%C-Fh_!F=x zUL!x?6YS$Vd)~x5*QvgMr@q#~>)TK4SXb)_)R*U}Z~Nf&{Z{pDKY{v&dFtCXczrum z-?kH|Z-l45&4btXqUzgx0`-lu>w8S&@$%sHtxbo>-w6U{L9$Vc&Q~w{qjo*Cj4Ku;WMlk*A4=Ex^BU( z`rW#-dnSz|m!VVl{36vo+te-CP2G7`ZRf~84etBSO2fV5%mLsYX4N*F^rykShkVX1 z+>^LJ3hohBExy4%e4p!#XC}TS12mrYSo(YBlOw{#b&Nf_F0SQ^Gx!)~)vm$E+l4(X z+pf^~OR}P)|2eg?lDtBS`Cdbwp`us5Wc(%x>}H?szue&ZhZMIY|Il9(yQBMJ>}x+U z2$fp@v&iWtA0|E(o1Uj0)HfT{m#?hEk0xw2a8g7QV|V=ta7ueHXv z!V}x*)vxo(r83Zb-@7(&uoqdLjh?X%TNoc;lYEFvIA2ovu9Wvmx`A|itBAFkdMkS4 zO4ciw=kaCp9{u7P){S`osAY}#FgBu-!#CucJa^ggP;fSWk48 zFsJRxBhNH-WKRjzYRy{DD|w!MMW|MD+O84K`%tJ>{`y^`oZnMJwX!cY&*$KmUUx<4 z9pz^z<=X<*DRO~P>sYzaf`QKigWvg1^C4?vvf#Ce|vPZT8vAMo(T=nyLa1THmc={iS_jS#4F5 zRomXkb%j-10iJ))xMaeW@9=Q@LpQ`UK2_{99jIKd1BbVqZKJ?1IzPjKB@_Pd9tFQ- zX->ay;t#?94aC<%cRyrraQq?UPQ%pT9Ea{){d5v*ynh^?wx<#ezF+`aT8v*O!1zpn zhBjM)8Xtb0Eb!o85vtW$*z)<*;R_pL>pV>r7aJY6sq(TiN2joDsw{9MTFPSl%NXk{ z#=WnPp7UkhwusHB`Kd#%)egPR?xWX0`kg~7vpw|6c?O1FH#_tiaOhRfMXv#eUN<}R z8gS@!vqP@|hhDWlptG_UL$AQoN3S{H<2Cw_bL+v&8p&z-!ED{i$mt(E!W>a-)|{FuKc1Sh&eGgL9^3|Bw4ge*(l7c3 z_rjskg_fNa)tM)|pJ0D5KC@Eh4b2&@ESHZ-acRFH{y^(mvWvB*rRT*%t<+R(=JCT> z&tWc6jM-G?oG`M!LOEm66E#QM^2{F3dz^8+XOMBUbLhJHq|$aZbD^QdlSPZz*35yy za*qrYJ-YHQzwr3-@5jV0^~pcYiHwcL-K8C`tix$Gs!s-bWXvf%7j0^+bD$MuW#3qz zsBD^W8e@GivWc|>uWVZ3z$ba+$|fV1D3_g29LHy@7uzo`WxOP}tmstAm85OLCdVdh zO5238t2CG98atwZ^_F|E9asnDT(|6=J*SdOXkLY}2}K*C!}=k%Z6f_gWgPpYsoJ&& zP2JM@WfQvgNgef;ArmT0y;jc-=AxyJEhzd{-R$*!A-)!RX-?}GA3lr@%b5VuVYf<0 z|JRdj{i6A3{i1!XtzS%rCUrlK@zZ&OnQ|_iF~-uRT)Oni$7%3Vm@$k1a~QhJtmmSP zdWeU5wj{Ji???3bs{ZAJe&>9~D&U3`<-OG9UoE9f}DBLtUbcFlI&Ms%FGQ^DM$xT!|7XPiN)M_UYR{#!hnJ<~i&A{A_OVt8F0Q*Q|9-7w zRUtRzpAoN1FEihcbiN&({#J2C=ADP{-Fp2)trx@8W7>D-U03#0<1bQscFu5}jatxB z%y}WlE3b0yHx2kPlFV-%#c5jYjAro7L5EhEe==CU@;bAZ#{%a;=igpAR(5*g!#1zQh(J*(xv3c>l(P)hslg=_tD_3it>wRb(R_i}5K zADl+mW9tW}bpdMuJTTM54kA|;vBy>G*RuJ03#?7O6Ul#vJoy>CDj2HZX`X+K?R6 zwPRvv6L}i?-m}+n6TGvjUFCqgzpS%kE?gUd_3XxhbbokeQ#;?f*FGE@9=7W^3df#z z(l6hGA3FFmbdC-yoZl&&qeHIGuy6Jle4o*mi$Z7G3E$gXeBXv`;Kg^ygKu+&W*{~g zzDGLvZgB9O$2APT6*Jjzq+vN{?KCEf$ve+jYysbP9Gc>U?%p^88j~-mAMR<-;GS#8 z%BlQi#oYJG)6%ihA2YbuxqSWR8GG;QadH2U&Z*1c&%XA@yiB`)r< z8-)AQh5O;e9A?n_{;(b2Z0Cp?**6C9Tlp^OtL-G{lJm($YS9S**3)o$FA3M{sUo(Go{FU=p!e6gteJlHvdsnTu?%b%~uZ~#4 z{T&zKSy*DwrV>XE&OIyZP%mZd|{8q z?zL8M&l6T~?^9N==Q%65ZT4-#b4AM;=2&vh4?PSckG9n z6{@ZF(4JS`+PQ+$`HONYLcv|=iw(*njckZNg}&hR9ibCu%T|YP7veucPP~|Wihad= z*DU`6>(9-6pI|Ji@3f-gm)Wyy89b-FEHxIHpmGVyRpawD?<3Cpxn-$QdXN9Xe80?j zU*o*LwJbGE@0nN4`zGi8?auobB*+=i@F?)-3cKHOVm|GNgS%D9rzfAD13!%RoIqW`@U*F9aXg@Qu13g>U zO7v`RdGpRYZ+h!$ggzMeT#Y;Wew4Aa{Q0m=4RuY1edb?%{vFSxxgk}UbvC%yFMEm{$ne3Hu)gCp7+-dNDt)CEN1^j2YP?9 z1YX0x-g%g~&|c~OzLtxquM_#w0l%PE;oi~$pGbP*0~tDE#F37O9sq_ z9+Suw)*jzZ!mHY+p}ZC*rXT*^#s0xtn8Uh|4a(D0gkA6ix<@xM<4Nc&2A}LfrWRnM zNgiokefM+X*}6V?^yYBda_TmDl4wskt9Roc-%}n+`iKj|GGH+A#Fr!2k>_f6xBNLbq9@XyTp#?p%?~bZJ8P}v&c`Pvcjg{P?#w-o z+?jhExidFI?uciWK@XfK+EU`nF)}BIeAo9nGDq*T2a!1&iIX_C%yH>kKJ5(p#=c3* z9J`!sZJl2r8i+4F1>NQ|X5Yw1XKD`lD!>0a_MXRoFh3)nF&nu)jojEBxpf-@75J&O zhJgGReQ(3(9WSs_fqD4skda|a=PCNld5UYSoJQ$G($7+V=v}EiAoY}41drT99qu^V zJe$^QBaVEkMn1W+R_6(M%boA`2bnCnxK(}04W-`l%yF(gvBTuTPv_s1zGHM9&GX2E zF~q2^;oKk5llZL*+S_iG)!IDc>&b%ex}WdQ!@oV5co@ocKhOLQZJqC*8R{P`XJu8q3sXx$>-vPt&^0X7}6j&?c=Tc9Z)v zpB!bm^CrFI%O~f}h_YIZ%Q(p$_^vBM_B^NWku$rIYl@B08L~OVQYC4}oilAb+xbA1 zZ&Ue#TzS3u0pxXh9lojPW=CElBQxc77kn(+{E%`zbALATXbwKi^uDcvmTLCjEJkj3 ziqDZh-nOw7p-rFB@ioR*8G5zjhotK-^Nh7upV?E?f4xTQ3?0sVB)PBgzJ)wqrDLoY z{fxKvn#s3w6FAcxDBq5*^6e-lTXUf8tC4Rxt3`Q`3&3#|aWw(smkRVKu100od;H77 zXE$=R%gWL^*o)P)RYIJSe9WePrya!wDPM_rMLsgk$%03*HLj0&KKD=JW7gato)ApC z16j3&CtEKzKtn3;*6YHbu2+8XfZ5NX_ID6lQ*Yq5dS>X}z=aRB6)@?HS(I#Ix{b_Uk@R|LHd3@zsOIuGrM0@Io(Z5Dy)vm?|)>+KlfGn{J zl)E}BI$pGf%#pvmQvULnF17tY$PtIu+FacriQX<+D_1Ua?u(%{CzhwFFP5jor7vgA z1)R9+^fuku&$}|<$TD!)u{8#cBaYwx7;t>)Q^1kgUf2EPn6>N}Cez*(XCJrUMW3QQ z`S!ec{u%ALJZ;+h`l#c!m;EW)Yp1=0XI!7nY|oV;$H3lx^-U7{q2%B zk{9C1pEE!1DL_u((_Vs4`&Df7YU9%m^z&(pf2#3m8$NP;+OmV>_ZD4mcKq29c-58D zvV+Wb__NJ-jz3%e5%b)`m&TVp#!4+7o#tQ1Kh}?b)s~yLUA(!NH8_{wPpCX`gw;kK z1zi8zvFm5fb?bNJSHJR-C6aYsxj2b+yxB&6W%u02zR{cvc~XI1wT*S@3Mi{vga7>o%Lt5x1II|ls(62FYC`}?Yu)j9QPj=2Q{A9BI(8VM#)>C&0XMprCZ`)q> ze0j}5#fwu^X5<4 zwF%s?)_A^eNb6?u`R*lO3TF-VRA47uXYh2geLfEMfeq8ij{cX%)o+L5U%wSQ^>*~T z8s-Ij2fOEDKcnXwA0N8lKJ5DU&}rZI;51&2jl!8g=g+%f3TrK+VyqK}C)Y9IV*#GeFOYX1&C z%C|1-3vlm<90qpiT@JR0-Pn+G$Px_57%jApDJdyyeV(=!JDBNys zK4kXHt1juV?b6Sk@#&zC1?Z6t*!*$usvHe|`~<{Dv@SlfWcfJcZXaym=m4;FPzStJ z>%!;4qkMQt>?ngvXnR0h(zebW6z-Ihcqw|np@(Z6dbnQnp#8);XU6rx+4Nw_UG9`C z8N8g}TEpCm4O^?YQ`t}v;f?mR=e!d;O81Wc#qi75p@B|psk5=6wp&)?lUH2zPG0t1 z8}s0q&VXWhj^>pU;lnFcuZLz%ln=$XnlHbJym}6r`W!UI`Sj8E$r&yB-isV4|HQwa zxY?OM=lRL=Zkkd@p6SPw#}2#b{TqEjdyG#yir$NkMemp4)4tfI_xFkOe9r^B#_YF@ z7w2FRPk(aDY23%jFPMO)&jZc^D)S~kkg`#)a!*kH@Y8Q4SK^X=qc(ph#% z5O`cahWgqVz zKo-ix&^3gt%1fTnhPoP`MR!<<)?KLsuTQju!!y(8k z_}1hhxe+-v894<{Zo{98pEtOhZwgZW{pY*7k|S%YjI8b7*6T<7$X9EB=+wol`s++K zEC$vwHqTWR91W{^U+lb}ILLcPr>#P#&FriB-qCHVu$?pCD>kLs8rHL%zC1g>+Q`*P za{rp|COhq3aP)UQ;O=DKu*O~s`Omlem^a>I4v-%u0qxrU%3<mZp+VPS$X)F#ZSXpej_~sIa3LI@+tSp803v~cKf`U^c*vl`p;z!euFU@ z#`$sbPrKvS!&s_4`GDSMEcY;$iCM^L`0Z-LV<#aGhqlxZAEc|~dWB+z;0MjI*K%Lr z?k{~$=37Y#Q3$aFJh5mNiym>&-uok_e~s!-;VvxEL-H1{XKPL zw*6pVJ+{u~ttY;81U|k~j6eD-1|MJH_g7wsyLOtW%dPbaO*TdOx$vZOY>9L~I zj#1A>Z#`3-da#L-w`bJDSY1p#5f41dtJvW5YqhsuH-E{7hj}k~YeqfqTG1lv(YagR zepM^)4Ee+_RmwLj)vkg?wbxu8NMIVnBrpL8Bqn?+o=v#t^^~-_k zxgrfuTV%><4m^x2Yxpi+OuwGDqW?iXkwM@&Hw_Q?8D{EPi+?bAV@5qsajuBq0nc7| z&||-#cHD72i~7vC@~m%Me`k;DJ)CJ(79W!y*WU$FKluaWTI}!|u?U9Oo^Z$ZtmuQZ z>H0)6dF^n4&1(_uCtr4HdQ6CGuyH_)N-<-*){5RO*gZ7q9os$WdOnQIoRA(9VkG+2 z(`-d=GB_A`On#ZJr!BJDg@-XA-lAVUk6O{MQBUz8y!NV7kC7oWZ5d+5WMxJ@E3N32 z)Z>*QZZ1^y+Z~fl)Mv(oXMJPB-Ujg+W71SsaaMXvJ`OnZ%9RIYe{0_Mu|N9-k8jeQ zKa`(n2Rf=Zp01enH?7}m-L{hT*?0kC%lH<-^A>;0zhv?^Un-x6@;Y_^lg_l*j4!Q1 zHW9k4^p#5XMcl*RuPcw8BjeZPYL!m$o8Gi8;e{_Bf1X#qD<@hu?d6aQ1v|E}x6u0G zBCS2MXJg)5(^HDo)%|?#_geXlSD(qb;{N>Bb9_TMH!QEG277+<5&!<1`DPbaN2lFT zd2v{N(hXIYh0z(^{dP7E%URc-LEeJyLMu8EeYiY;F3!1qadd{=WAJ4l*OFl?WpYyk zpZs*w;n#`Sd72BF6&C<5>o}h;j2(eYY@5BUR5G)zVtXlP84`Psej=Twow;T;IT*CJ zSUJhs(G#Q>Du-YfJh^)ex?!QUe=V^$)rHpE-Tpvp_mR3sx~=a%JkiQ+oWV2f$QLGz zvVxnjTk_DwwBKcj8KWURS8rXis+qIdEa1WSOwK|1F=QWWEp|xD7ZvY1NZTeqA?@#B z>~#+E>)0X0U${i^u#@~PtITus2y}?0=3aV=)v}oT0*{U? zKAY&!vT_)FrFUR@o-x3Aa3zdqBe>N3s62+@5yT1OCm+W(1RZH?t{@jt19_v`p|=G3 zxM-)^Lqm&MbMHjYXPx@(PUuAb3ei$Mv?N`98oK8?@*+0l=PQNhv+;o|AL9fQvt+Ns zOE9a?p0WfFW#F4pNA_*C`_=h?J&;UjvD>nqgT z1+PSo+`GIBUW*{dp3pOB-uTiOKikJ6yl1zdgZAy4NHYG~n_K`~in&3y^sGhy@8$X! zzb~Y$#zFANui)m8?0~N7jZHHm`d{RA)7X3M7i8!c%zF0aw!L}US=cbxEwW*{c;@De z>_p$oCdamDP5WfEW?!nD2+)>bckQbzjl_Rx!b5#Dl&S$UPV#be^j z|HHlf<>G=KG)FySm$` z6J9=QK0(GtDwt0yBm=%@<`aEu%RR3gwfTH-IcmR8&?m{l8^NdcsOf%YAD*q2Qgd(1 z#V-$l^8_}T>o>@ZFQ3nvf4}{+qHXwkEAp^>l(oMVnjX)Zg?x3w^)>t^cjOm3LjkG)neTo;}tn=dDNnnUxFwe9GUMH}u}RljY=_Y*?~N zaUAuu7l+sXQDXs5+v8*Ba}H^bXT9HvE414zxRWykprP{de?IK;N8pD5{@mzpa{U=y zjBzZ1mf9JIaq0s!P|951ZqErDoH=2G^oqsA;_6)Hwa|j;pF&E`_ymm2unZ?XQ*p@E@fTMsMIY;5w3iQM8iHv)k@fH6+ zub3dl@`qf-t8UDY=ISdvH2Dw60hj+dFY_pR6mQ4yDNmrkUC86J@rOK0?CD%&!t2Z> zdH8d?iEYgF<)UNe_S{0h@3pKKRc4N(lMYlq$CS_M8H!)n^*w10DQ8bbi21FHF%>=4 z%O@PNW1?Mpu7)1k8C#8613I$CYd!QUnIb%|4}`Q2gq zy^ea74`&>{#d3JzCTL4Ev>9ITGZ&XI_eM^!X0GGCk%j1jsug;tOQZ{{g z^#uFk^2``-U%->MFVdgY7eCkI_67amAJrFVKC>@LWc&p9MSTf#Z~6k?c>A*5;H9rG z2kA?yuP@Mdzj-rbT-?6wZg9rC!rH$LneXza!8bUPjMBcCs|6==O7Hu|{y1%jr<}RD zY2dlprV+{hUnu4U7=Od{9doa_wxH!#+>3VKbl(3b*H+K_Hs}3oTodr;E9RX&XZ(=+ zO#Yn7`6}+5aXS1N&sF~lj4o6W!bhd`XUp1u67-Wq2S663rk=!kGb2Kq=KRe+?d-bg z=>wasoXNY9XSvuv%bFW^&U@gQ0|mb9$wlCA8a&ab4@0jV%&T3@o3F4wDOj2vyHb2x z?crhZ^HONDd#pXi9q0o0SXqtAU2f{MCPb$}>uP%q^}5fRp^pUgp}N%;enKN3GV9(> z`|LkJKg?-$>vm!Nsi%$eXye}X%XiK>6g#lX%ATw_z%J)EI({WG$c6phD-;(LXr*sP zZ(&bcqg&@QeRT$+I!pHKXCB^p@6GE>oeAo&;SZQPCC~fnL>C&Y&VTXLX@3t+9DHkU zf_wiU_lmWVtI!B)w`jK`Ov{l(8gY9qrf+0@@{CS5xS^>F6w-FleePhUj>|bi?X9y8)q(p zwiI*V_`|Uoe|@-jWf8HV^3RC(&MAa;(9!T8x01)YHE?9{@=pBDtSP3REZn}Ti04{= zDf9(cJM!%ha6a&E^tl`#XAZ%m9oPn)=)9&~-voRWp^ZDhi~IW}`YKq9m;>Brf~{0} z&+=LCcIfCP4~^Dvf3q*Qku&Sw?(q+8UG5vYyxiyOc@H@uUY~2*%uVyLnU!;AJZ;D} z>U;0#RrAr)^?oexB`fdaoQ+&`S>$7AjcyRl)mw7^Q?}`P%7J5+j;9U-H+yrlwGp=jkJJZf0zZuu9$OLyj-V1MN?w-zE zEZ)>#3H@{8!kXqgxVi~k^XX@xTvAIFvDWBgeJ5;!0-jUBo zXQd5ejw|sEGjrM`UuaW)z}lq#>WqmU`PPeScP;j4nU&kRhd%E)a^LcO^!WkP=Ui0b zeADMAoIbm_6JD<9>wh3R3AoOMkC(&8>OX6W4($X?|B;||NwSDa8=PK3; z{yT@O=B2aDZ`-aMYVPehD$m>xY0-PxsH2>6SzJ%&Pjutzm)dtK9dqp|)a}NHvWB<) zk7=KfEAQ?@c4f-DMA(*hl6&pbIRk+;WySJzFTw_pomc`dAva8H%r%l9&OSB9T{5x5 z>Z%eeZ?E+C-Pl*q9CYn(`2hSkL&uZ&PBv6K zvNVbRKxfX+L!USMR`RU{Tlh}C&ypF|0%FZ)medkE1#itqPrHvjA@bYV>u~P6VxZ+7 z=`)j{Gsn5R>(BeQk>BeFSV{FLfD zvP5SJXwQiHAvrRSHkUcF(Deh0$K?ke;=>P&ER-MkDqsHcX5{B&;N6CNnPKI%66eu6 zLi}kB#MOJ@PYeM6>%c$G1OHEfzutk* zYZD08eD*^ToA&l@e_rd^zP#nK%SzDWZ$g%8j+uwP+(n;XV=mc^UXXWW*>ZTiRWTY@ zab}J71o$O`D08(hr?tmFq_y=(BV}@0_fck@Qpn$?f5dD=*_XL+~Nj@r_{W40&%uic+b{rZD{a8+h|wDl!V zTPM^Xr+)f#RQ>m4)=z#1^=G`N4dRhE4c4DMgN)A(`r~7K3h|Nj>l3^?JF`vN%k#9i z2i&>%I#&Ia)a@H!jIPbB-x;G1*n??k4LZ;mb$iC>nC*c-r$3tt*^}0wW5PCkMG>y*Dw zb>g>h>ong@;X8aj^lj5o-_f7&h0b>urq`N?wX9vOv!AUqqA%CsOTu?l0*>=~NWRs_tUzlKf1T*widEJgk~&i;jfcd^} z{?YzUXP#?gZ>o2mQ>?I=7@YrH`7^`B3#9j3dHJ;lzoqzfExh*U@@o$^oy%KEXj{7R zQ9L4k9e}rp8`xA)$l8H*a`eFW(mXOB9j*f&A)X>AAE9_;KJ6@lN3!7&cytrFTov=5 z%7*sTeik(5($Q>a@NDSF@CdwA4*wQoPr7`cDUZaziyYqVlSlB_+qBOpmAi9%CCc;iuuED-g+;--ER8VZsj!kv}U+vG`321%YR@KD86A0Jn}es1-?bo zOev7{E<$fUgJ}ra3|LXt8^zF_aN$6R$eiSVYHg^!yXY<97 z=y_hgVBP&FzJRWe;)}VVTE*MC{9)w3!ym5v{|59V{t!=Sz7cG5wwYKQGsnIL&x*#C zH%xQk7R|4JCVgiggTBA~r_#6O|6if+cEz+fa(uh)q2pJ%iss+Z75aW#SBJje!GrG|avs>|m|ybsJfd$G&sL&MPi{NJ}~M{R7M!%Hh(9Ue|^HC5ca_ zJ&Ygb2H=>CZyDWhAR2e$Z=~mqKN;V2SnFtko<``o>jv^jAm@>FZS&^d9>k|xQ%~P^ zPiIe&FM$6quk}9se>?E`wIDw`@Np=HTTh2uH zsxORwbQNcVa?Z4T&8$xvpOC&)9r|ARb^_>2tI$WL`tfs=Bfo)3yeq$w^vW{)fgxha z^qu@l`nKH4HhZ5`$4S(o{atZ%Ugc+yyiH=CEI!jBcE2z>=8wd}ur{MumNjgGt>E2XDt=2up!vD_);{-5ev~A{^;nIyU_vR zhj&l6+7@mb7p)u`iX|EYv0iv1A=a=~QsR#Egj^v#WjE>+%&HYaZd-w6le zhs^yXKO|N`vZHT532+E6rrp9}@HXx8%uCb4%NpSY{wN`q_esSU!lMJy7T>ylGyA>O zuP*U-0-5LbVcKAQfX9#O13W7GtzLcL{?qiKM6p^1Hml_v?)&DI;WiDovhUFF!SGOO zuz1TIW-dwZ57D?HGutFH73Yk+Y?HivaymI|N0LJnoFykxKjTqR$bPKxoLz7Z_ngnG zynhkCiLmyRMBlHnMnvTs_tIU<5j&53Id+DzxtSj%qosT6I>(9kO)zHSA;~29{@l1< z+2QMfLF+KGo4fEC?_vIdE;h_zo=^;KSDxaE$;po0;r=%EG{1@aNOcWk9SWPj82j3^ z>9j3B#B1o$3G95WPbTBTqmLv1T|LJOryG|n-}+W`A;l(c`*3(v@rthy$Lqo?Uwrvj z=qvizQQy;+`JS~}^Zgd*d!DOJ*&OEk3f9P2-yHP&uQ=ataK1->bieOpOjgrp;Y+@9 zt@o>~*YJ=3l=11pmd}yhK|kWiWx>CVaeakxeU)*Qtf*vc6YP=Pf?eT-!HgBK>e~c% zlEGQqaHAL6JeOnqIB~tF{j{uddC`tj77g2Hy(qV}&AT?Q+V9OAVe2ey)dT7*cDXuu zNHG}S=DLWu;M=*XP3d5_aW6gX8@i&4iN0oe?!U&pbhJ5K4Ier4z#;b6X&$&_5PM@j zxr@B<_lj|M{a-`LB`$k<8$L4eRR=O6iR@GvFCLHS{|Y-g5q5h!@F--+jgT4w`kpBZgs9yb$?ce`)j&ciD4>c$?f@GdqCIYwycX)@RpD zckKN;9J?mWZ^ho0aK%=3Wkn9YF0J9@TmtUtbJi4l9&v4+B4TNbPXJ$7c|HCQ?aj}S zOJ@FXG*YRu(ZI7BL^TL+_=woo?o;HRx``XhU}TRD5LPp{)?M z)j)VPV{)N`BY4Wik?cF;Lx3LSlk4*#(4V7x2;j`|A+)(Zgs0(Q#U{(=_87iB&7-al zq4P724*|Y6J_LLx{y}{RHm~7BV7zpGhvJRBK7=&SI=-~FqkUt@}g9H1}1^y_mQYYlXRgE4L4ZEx>RK-^y+P zmbMD!^+M=R_mzj;y-;NbxA^V7P(NW$uy_CHeHr^PvMxklVQyFiZI&~ihz^%9UzT9^ zD9%3a$R+s<)c3iXQ{kID=8~yaptS+{#GKyxIB`HeVt%C8i^jKzm++&<&yv2(KG<6F zUcj@9&V&DHk64zTD&mb4H@S$qissFp;=>1sZoJ8d@2`HWzB?n@#doF9W|3u|IVU=I z^~EH9Z|MaSz=!UQpPO$JjH%`cd(Lt9c4W7_LpjCr?a`G#@4WDj;0anx{kz9L(0;KQ zUlP5fE(AAdWr z<^p#w@e;Zp$5?6%!{{dXFVWMGC+Nm*TkF&oG`?t%w*GB^wp<)3wpDF)(3b8+H)?Af zZD~K9a9!wWYq{D&Rwjt=G2=1RJ_F9}*FA1q)_Eo`TEBiFkJQ!<^^5!C_3Ku(1${{t zs4d^9{y3aFKwEwNf|k_Qv+5W3$LrTjwS{danPS?a-)^6-b=wJjuCGsh?Ob5DvqJ4~ ze~fn8too7BFRGp5H2#EN+Tq<7qZixlu$FM4+s=kQnn0gYf9?|QxIYH&Ogn{Y2c0~l zKc=122WZFbPqM&nr-F8LFP;%Suuhd44?SqFYz6dyov=Z6LJ|1Vd@yP^`(Cks6X;p( z@R$4k>}jc$+TS5LhG|;Ox^a$Ef4CCsGINRL>ow zjyF!Ajw<#G|KJ#PynF(6C@1{`1Jwa9ipE-02lt}m!D#F;;JEu3b$tH>>S)LRTQg7{ z$T`8WOm%R7EI7zdzwz$}ssnyk9e1b>?vGgqa(Tuv>X>r^b(F!=QwFL7IVw2*Ms;w1 zEI6vDW8y${AX8MwRMp|gm%+y69_sk)0Ci|@j_SCWI&?2tC_hCp@*@lRVb)&pH`(+1 z^gdmuA@}gg&t${{%}1Z}>U&KiL#Yq3eG}*%A#~z$CpKvO0C6F*m0tsf2O^$5oLZy3 z!H(gXOM89#<_)1*o&SDH7H71wf4@2Fl%B+q-kCbfW>gloqDL3C_}8b89uGeJX)$7B zfbXa{55e~X;G3NZ-())`r^E~2uA;Q1&gJ(iyap8EzEzY*+P7JXGX^rcv!dSZ_X z2cfa^f$5{Ffobeb4_qD4SbGMpwt8qx<158$ziCtuDFzCxZFXtIuPSAUXw8T0cf)f4|w^D2AH!)AWgdY_@-`X^L$7RuEs;rS^JxXyc=I zhaQ=BFT_*7nFEY{;^d~7_w3t5NY_iFr$^2=l~E}ht4N%~FBJ=U8iM(fB$ zq_yhD#vA|NhCcrvu?K?VF}@uK46E@?t|_$49@l<$nxnh^H|6_u*E_2Y8awUIaR-G{ z7q3;|^{1@!Wm)wP$LVjtYJX|5mDMPlweK1D)UyD%d>LFW##g0SzQ37l*RQ+=UY)H_ zK4RP+K7WbL@r;C?`#BGV*wt*LaA6}B+#c%M^_srnM z--nlK2QS0H3;al*FOQx_pYcoJ=L)#KfhN({R{Ea+cEOQ^ZaS_Zw+(psM_|))`6CRB zjI-X$f21-xqglR(ENuA%wB}PB8L>hE{ODV!VatP4?^uXGb-sjn^oN18Og_WmQ+Ix} z4Q#we_=m*M4llN7^5WP_H3E*N7-YooK!`~T*;F~w*2TfV30rNsDI;i@mKNojiS>Z=kL^5QB zADro#@FiKn8Z)xO-*Pqbb|AWR<6?(+XtRU%zmrKXto3B(*XX6cy_>CI@5@%uJI2+F zaUf%^$R1>_u*W!H#yHzMSAaLg+$xWz=8Q7N+wA{DE-QA2IkstW=+vkg7sjZAe0#!w z2Xd+dm;#=0b@%E>X1)#`Z1r<)Dzu<=z^Ukbip%Rq2ehGe`z6RQQy=|PKMxgC-VZOx z2mf*?Xyen4i53prG9k){Z!*iofh~Rl*uwrP>!(R8LGBV^_wt$jbT| zHpWiov{70^SI!cmz`E}>4Q0g%8X4U>|;;gs)+T7=6n`e>8Ir1Af$#2XYddZK7Z;{@jGLfOv zjK4?s6+^QXXVtW@0hosa^F#UlV2*fTMsD>Kf?vTL0cQO+u-5wwti{9X4NeV=*E=xY z;J`TA1LGJ6M*pOKFh1yovA_dkL3T=KiF$GA!u6?ODlQ8y9(&b022Yc180s!?U|HKA zmKrZC7kgm2*umE}7hkyp!lf}D95&0w7i&(UL4z;mAv3072VW%~e1#nt!{_$HSD6>a z=^hxTJ23i-oH5P$^e~1T81WuG4SbOJjcdk1V4En9w$05 zq9+;{7kXff4Hy;@_-(aJjOu1Q{aTPIB08oAqVR zlr3}kv}�R^1a$o00PkUfebpdE0!-)8_xt04N7FUvLz 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 z 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

)`jol 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

){(rk{ua9qn|^ft2&zQ5OX6yiJ1wT`c6 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�M5f25vSoFgBPfz6+nM4E%HEn^guriu_~8 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 z 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 *bSNFI^z*T^uX4wE9zJGY<&h2_iO z*W{I{_&~_HrO^9B$QZ|e+xd_1-y~zCleK@>ZOD{PWHdReSuu1<6d4ggf9m{8BU_LQ zF?5PxF>8LHE7`BaD4v#Inu$N+_!a^_z9cvQ%a_42>d-bLubT#pP|3OzpO(jzdPO_mcF263!`Jhq8 zlm{(-51ZbmIeU+4*FHGv*axcBkbR)Bbuu<%CqVDoyH9q)gREI|d9z$HgL{$}9r=!( z;5^%JCx8#vPN+Y{yT9?idn!A@!Oj15^sn-#>yhP>k^TC2p61ipkHxVQK06fsJMU!u z`>fOF8QTerL*s1WJ;~UU>;%T`wG%$~e@n)g{o&EL=coW>W3U0m&?y41b{domN<(j`G@NcSodHLMI@+BvY zd^uKX$Cs}fBN;QQ-)888zqPlobb;Tt7aByv0c>$*$A??wuQ*2Fc*)2TjdysVq_n&wubbSAo~@Y z?nd^hPqXF`c_(~4LM?*q=sVcQPcU^OLymwOJ4eeIIPi?tL5SA1&O|h|3OV*Ta^*;# zz0UISwKw)ofhHeYdttBEMjip5+RwTgn9A@&MVnfazmlAe-q#u+VrVmRSpV>Ar;S|J zKva^ukz9^TPA@nar^b&$F7E-iqSpX)uK1Gn5mCGSHt_+5riap#8 zY^n3*w%v|gS(RHV@nPxPk(~P}-Rjsz z4YDsDcI3Gq+oBTNLOiNCogdoQZ@*(#5I>$FI?aRDZMkj7hXu!e&JIxS&gf&}a`Ff2=Oy}aeTI|#f7WPv_ooP;&pOzvR`EW`=n2Gr zC)U{O5SMEo8)CoPM{as>2kTg)578_VF|&wqgbA^Z>J ze;EJdyJm-4$!oV}%;0k?pO^5tgU|8sBl0a{5}(MojH!GgYh(9Z)!eb=>gLX^*EDzU zxR%^_Ccm@zoz3r@=GgL3>ynY7)f>QZ%>Q-e%34Kl@g<>M>_fb2rLOZ`*XfhJ`K;Z= zzp&+1q|z5g&)=_4*`*ioJ$BDr+P{+a=h6Or`Xe@pUD~E2by{3O)_t&m< zFS+C(eT;qJy?;6BJ+rqCYko^OzvY&jv02FD$DH5aO!>V!<@Z-oey`-W=vO&j@$V-! zcku6h>GY40g@f}GMcS(yTcwD(M4t1RcTD(-GGF=DMdT=~7Be5<=TgQw4nEy`j`qD{ zjgNZ|98BS!;;Kc^K$+Tu226V+L&tpJk$2w8XCC|CVSX>*9em!=c3;y5?m;tSxR-7B z#d{M5?lHe3xo6Xh9k-V(7cG7D?RU21gU9@u3E?O@Fj4^jQjcO?5ujG(+q+_M*=;&I z!Qb+uhm%`2@O+dp5T_vya(+1D{da6!O8<_q@p**xcSiq?2qmBL@PoU*XY)tI69emW zqu8g?llXwUBeJKE+mdn8ljU#Ev32S@&VC!F9?D*88pEDc%xc3(Yj-B^??E12#Qp;I zJ~R9#cOPYbo=3)HBR5nFUix8dPvnVg&)T!CMK2(Wwp%#|6YNKm`ToW`IIH*I^Te%% zW3@Mg_MS&BW%G{SFQ7K%Mc#RVHaS=CU?=P3I#1ko#}2+{vIepbTJGGr0UN>BW67SC z-!5Amd$9xjc7k7GVHs%-R1IyG7nw} ztu2R^=Rph0(Y4EI*L;T-mZJl^cy2kd!o8NSegog-x6Aj?8rFUIEpcct3Ju1_;&<{q zwna}l^g0PUNw{1}+wy@5kUtZwz`-l|*L>H}ul6wNz$Q{%)rOrLAB;kynpf*R;xA)3 z4+_1hYq2r(r+ML7trtIeK5QAJ>z_`8zyDwzp=-bY)FVHnPet2*8!-^?SG)|{DOo^m zqhc$S0e^iZbX5smRROQgP!R9euBbG*B*l$HOLpB<)}VN%`0~S?Z>QYsQl6>e-R=0M z%V<|)t^gIbfS7)$;juB|8?*|G?Ul}Z8))!26s=2dC;?R!`>K93_NLW z?0P<{#ndlA&sEU)BZ16*9(kUzG;z&s)2)5I)M@j1Cq~i+uT(=9wyd|~w62X(>)`cC zaO&D;_}<`ifc_$jtyXH$8p>)FJt@f5PJ|O;#cpBO~NuMkZwJ!S?;^M4Z z$tOnc>XSy+8Cgp|7LKyc$g5ArHl@^L4YOLMkN;V44hyyZ&gC7Czj`||)eEom*j4md zJO=$*ljZHR!OEEuUZ2wE-x|~Rd4$0pnZN^^-X#5ucA7zYAl1Ka38YLqE}BHjFO{MrbHHtho6G;2m@YGyV2Nm661!Ir%>@=&D z{7!2E9Z5|>Yhu6E+d0N+MeYY#UlGIx48}(hgBTNPeZSHQ_tjcvA4OMQ-;O=zjbBCL zwB1G9J+xg!+oe`(nW+i7FubIe{OKr5`{6g_KRd&$S^VkoKf-6zu$n_B-sU;JNAQm&@3o&w5w-$)c9HaKCi-!l z#`}sVw|R7d@ay8^s!@aGjggns-$%PR_o#>S+p57yEqbMkGv220yC@C46#2p-=w&_n z&Cl2r2mCPbXF?y_p%2x|B&M;}+DjwICFH{ZjX-m*pHu~n=sdV5CB*C=CB4e!Rhwg)S%%saFuBmTF(8IkF}AoVNZIP$$3U*Z*Puf!~0 z(BNd$cXm}mV~JVC!NldxoUC$rct|+b>NXOICi-&d@0wYCm%*%>Ym#2 zfxFS*Oyi6KM{Ig=FfVW4gU|fg)g}10>2bERzJYS2 z@(m_1*RmDt++1eMYrBYB1+1r|)8W<8*0EY*68WRaX|R_32y(LvTeJK6(6dJv*Ab`f zBkW%{IkWBalRvrkwWI7^SKu4k5=Vv{IV%)A@{Sek8W{?9y=MiphqQgZV{Pc(4&aHr z4R5{!Z@w1_szy#V$%AY`r)|-*YTOk&aM!C0_RAdo$-Z?nsN-uyUR=#*KI01UY2<_B zSGnr~4zY&R^$n1bgW?PF4_*RK-t{<{^ReN_Td!v2H6GHwLPx=gY_W`M$-T_M{{c4@ z_>>9RAhPFyMe8O4@V?eHyqSS54-Iq*&-hV~oVfnJ=a|b!PRzgWxquaXK8L!OUzY@z zB0CBtKY1o_1@bY3kBiLo(MJX__~}pnY7KGxeDcz9o~z_~E;6+dzIf;|*3>Peo`BDa z8}zKbpR|qFD|q)8u6d?>%v{IDx9_>JtUH9C>`?L-DfH*c%jR65;8=C^S#rBB6D6cc-$nhR&k9P5=5n0r)e4`DZIa=B%A2 ztuKN$UEPzXGs{*?4L3q(UfOx!!_X$Sw)UAf{nFPB^jkr{C49&4Fmt6Cc!hElPQTxy zUwt2^xhjBf=v(m|)f891Ibh?(vEM7mZ)(2WIC?$rie9SV1LZW!h}mciF6`*fx2$hC z_AWBQwd+%1cWL`I)_{t(uY6R(ZQ1G%JnLKVdNuv zN$cJf*X$rpHtol~TgpdE*4eh#