From 9bca523caa40fe23bcf7c9ac8a61d8295f6e505d Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 13:44:10 +0100 Subject: [PATCH 01/21] q --- api/payments/quotation/internal/server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/payments/quotation/internal/server/server.go b/api/payments/quotation/internal/server/server.go index 61523efa..74ea8579 100644 --- a/api/payments/quotation/internal/server/server.go +++ b/api/payments/quotation/internal/server/server.go @@ -6,7 +6,7 @@ import ( "github.com/tech/sendico/pkg/server" ) -// Create initialises the payment quotation server implementation. +// Create initialises the payment quotation server implementation func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { return serverimp.Create(logger, file, debug) } -- 2.49.1 From dc6a4224a07a66a955514babf2679f63e237d311 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 13:50:58 +0100 Subject: [PATCH 02/21] q NATS --- ci/scripts/payments_quotation/deploy.sh | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/ci/scripts/payments_quotation/deploy.sh b/ci/scripts/payments_quotation/deploy.sh index 7a285328..6b1dc96e 100755 --- a/ci/scripts/payments_quotation/deploy.sh +++ b/ci/scripts/payments_quotation/deploy.sh @@ -32,6 +32,9 @@ load_env_file() { done <"$file" } +. ci/scripts/common/nats_env.sh + + PAYMENTS_QUOTATION_ENV_NAME="${PAYMENTS_QUOTATION_ENV:-prod}" RUNTIME_ENV_FILE="./ci/${PAYMENTS_QUOTATION_ENV_NAME}/.env.runtime" @@ -51,5 +54,7 @@ PAYMENTS_QUOTATION_MONGO_SECRET_PATH="${PAYMENTS_QUOTATION_MONGO_SECRET_PATH:?mi export PAYMENTS_MONGO_USER="$(./ci/vlt kv_get kv "${PAYMENTS_QUOTATION_MONGO_SECRET_PATH}" user)" export PAYMENTS_MONGO_PASSWORD="$(./ci/vlt kv_get kv "${PAYMENTS_QUOTATION_MONGO_SECRET_PATH}" password)" +load_nats_env + bash ci/prod/scripts/bootstrap/network.sh bash ci/prod/scripts/deploy/payments_quotation.sh -- 2.49.1 From a09fd550babeec8d86f8df63246c389df89e4965 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 13:51:49 +0100 Subject: [PATCH 03/21] q NATS --- ci/scripts/payments_quotation/deploy.sh | 1 - 1 file changed, 1 deletion(-) diff --git a/ci/scripts/payments_quotation/deploy.sh b/ci/scripts/payments_quotation/deploy.sh index 6b1dc96e..921a5931 100755 --- a/ci/scripts/payments_quotation/deploy.sh +++ b/ci/scripts/payments_quotation/deploy.sh @@ -34,7 +34,6 @@ load_env_file() { . ci/scripts/common/nats_env.sh - PAYMENTS_QUOTATION_ENV_NAME="${PAYMENTS_QUOTATION_ENV:-prod}" RUNTIME_ENV_FILE="./ci/${PAYMENTS_QUOTATION_ENV_NAME}/.env.runtime" -- 2.49.1 From e717cabd0fd90485b222c6a6ac65c78e45cca3e6 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 13:52:53 +0100 Subject: [PATCH 04/21] q --- api/payments/quotation/internal/server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/payments/quotation/internal/server/server.go b/api/payments/quotation/internal/server/server.go index 74ea8579..61523efa 100644 --- a/api/payments/quotation/internal/server/server.go +++ b/api/payments/quotation/internal/server/server.go @@ -6,7 +6,7 @@ import ( "github.com/tech/sendico/pkg/server" ) -// Create initialises the payment quotation server implementation +// Create initialises the payment quotation server implementation. func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { return serverimp.Create(logger, file, debug) } -- 2.49.1 From 76548032b3262abb32026f12e531e67f91cba4e7 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 14:04:38 +0100 Subject: [PATCH 05/21] q --- ci/prod/scripts/deploy/payments_quotation.sh | 14 +++++++++++++- 1 file changed, 13 insertions(+), 1 deletion(-) diff --git a/ci/prod/scripts/deploy/payments_quotation.sh b/ci/prod/scripts/deploy/payments_quotation.sh index f1bd3061..0637f8a1 100755 --- a/ci/prod/scripts/deploy/payments_quotation.sh +++ b/ci/prod/scripts/deploy/payments_quotation.sh @@ -18,6 +18,9 @@ SERVICE_NAMES="${PAYMENTS_QUOTATION_SERVICE_NAME}" REQUIRED_SECRETS=( PAYMENTS_MONGO_USER PAYMENTS_MONGO_PASSWORD + NATS_USER + NATS_PASSWORD + NATS_URL ) for var in "${REQUIRED_SECRETS[@]}"; do @@ -38,6 +41,9 @@ b64enc() { PAYMENTS_MONGO_USER_B64="$(b64enc "${PAYMENTS_MONGO_USER}")" PAYMENTS_MONGO_PASSWORD_B64="$(b64enc "${PAYMENTS_MONGO_PASSWORD}")" +NATS_USER_B64="$(b64enc "${NATS_USER}")" +NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")" +NATS_URL_B64="$(b64enc "${NATS_URL}")" SSH_OPTS=( -i /root/.ssh/id_rsa @@ -68,6 +74,9 @@ ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ SERVICES_LINE="$SERVICES_LINE" \ PAYMENTS_MONGO_USER_B64="$PAYMENTS_MONGO_USER_B64" \ PAYMENTS_MONGO_PASSWORD_B64="$PAYMENTS_MONGO_PASSWORD_B64" \ + NATS_USER_B64="$NATS_USER_B64" \ + NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \ + NATS_URL_B64="$NATS_URL_B64" \ bash -s <<'EOSSH' set -euo pipefail cd "${REMOTE_DIR}/compose" @@ -111,8 +120,11 @@ decode_b64() { PAYMENTS_MONGO_USER="$(decode_b64 "$PAYMENTS_MONGO_USER_B64")" PAYMENTS_MONGO_PASSWORD="$(decode_b64 "$PAYMENTS_MONGO_PASSWORD_B64")" +NATS_USER="$(decode_b64 "$NATS_USER_B64")" +NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")" +NATS_URL="$(decode_b64 "$NATS_URL_B64")" -export PAYMENTS_MONGO_USER PAYMENTS_MONGO_PASSWORD +export PAYMENTS_MONGO_USER PAYMENTS_MONGO_PASSWORD NATS_USER NATS_PASSWORD NATS_URL COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT" export COMPOSE_PROJECT_NAME read -r -a SERVICES <<<"${SERVICES_LINE}" -- 2.49.1 From 57914c07543e3bf030ec0fc26208f10b177d3a67 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 14:05:24 +0100 Subject: [PATCH 06/21] q --- api/payments/quotation/internal/server/server.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/api/payments/quotation/internal/server/server.go b/api/payments/quotation/internal/server/server.go index 61523efa..74ea8579 100644 --- a/api/payments/quotation/internal/server/server.go +++ b/api/payments/quotation/internal/server/server.go @@ -6,7 +6,7 @@ import ( "github.com/tech/sendico/pkg/server" ) -// Create initialises the payment quotation server implementation. +// Create initialises the payment quotation server implementation func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { return serverimp.Create(logger, file, debug) } -- 2.49.1 From 853b8550495d207c328e9ed9c633bfba2f5aeea9 Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 12 Feb 2026 16:06:01 +0300 Subject: [PATCH 07/21] localization fix for amount --- frontend/pweb/lib/l10n/en.arb | 2 +- frontend/pweb/lib/l10n/ru.arb | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index e0f7245f..0b9a2966 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -511,7 +511,7 @@ }, "tokenColumn": "Token (required)", "currency": "Currency", - "amount": "Amount ₽", + "amount": "Amount", "comment": "Comment", "uploadCSV": "Upload your CSV", "upload": "Upload", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 051b1151..ae3d480d 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -511,7 +511,7 @@ }, "tokenColumn": "Токен (обязательно)", "currency": "Валюта", - "amount": "Сумма ₽", + "amount": "Сумма", "comment": "Комментарий", "uploadCSV": "Загрузите ваш CSV", "upload": "Загрузить", -- 2.49.1 From 45d3c3145cd10064baae28d0f88996f903a4a1ed Mon Sep 17 00:00:00 2001 From: Arseni Date: Thu, 12 Feb 2026 18:48:57 +0300 Subject: [PATCH 08/21] redisign multiple payouts for better ux and small fixes --- frontend/pweb/lib/l10n/en.arb | 3 +- frontend/pweb/lib/l10n/ru.arb | 3 +- .../helpers.dart => actions.dart} | 2 +- .../multiple/panels/source_quote/header.dart | 7 +- .../multiple/panels/source_quote/widget.dart | 37 +++-- .../multiple/panels/upload_panel/actions.dart | 47 ++----- .../panels/upload_panel/drop_zone.dart | 130 ++++++++---------- .../panels/upload_panel/progress.dart | 3 +- .../multiple/panels/upload_panel/status.dart | 7 +- .../multiple/panels/upload_panel/widget.dart | 24 ++-- .../multiple/sections/sample/table.dart | 2 +- .../multiple/sections/upload_csv/header.dart | 5 +- .../multiple/sections/upload_csv/layout.dart | 38 +++-- .../sections/upload_csv/panel_card.dart | 7 - .../multiple/sections/upload_csv/widget.dart | 7 +- .../payment_page/method_selector.dart | 7 +- .../pweb/lib/services/payments/csv_input.dart | 62 +++++++-- frontend/pweb/lib/utils/payment/dropdown.dart | 53 ------- .../payment/source_wallet_selector.dart} | 44 ++++-- 19 files changed, 226 insertions(+), 262 deletions(-) rename frontend/pweb/lib/pages/dashboard/payouts/multiple/{panels/upload_panel/helpers.dart => actions.dart} (92%) delete mode 100644 frontend/pweb/lib/utils/payment/dropdown.dart rename frontend/pweb/lib/{pages/dashboard/payouts/multiple/panels/source_quote/selector.dart => widgets/payment/source_wallet_selector.dart} (55%) diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 0b9a2966..60811736 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -348,7 +348,7 @@ "description": "Table column header for row count" }, - "amountColumn": "Amount", + "amountColumn": "Amount ₽", "@amountColumn": { "description": "Table column header for the original amount" }, @@ -515,6 +515,7 @@ "comment": "Comment", "uploadCSV": "Upload your CSV", "upload": "Upload", + "changeFile": "Change file", "hintUpload": "Supported format: .CSV · Max size 1 MB", "uploadHistory": "Upload History", "viewWholeHistory": "View Whole History", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index ae3d480d..492a9693 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -348,7 +348,7 @@ "description": "Заголовок столбца таблицы для количества строк" }, - "amountColumn": "Сумма", + "amountColumn": "Сумма ₽", "@amountColumn": { "description": "Заголовок столбца таблицы для исходной суммы" }, @@ -515,6 +515,7 @@ "comment": "Комментарий", "uploadCSV": "Загрузите ваш CSV", "upload": "Загрузить", + "changeFile": "Заменить файл", "hintUpload": "Поддерживаемый формат: .CSV · Макс. размер 1 МБ", "uploadHistory": "История загрузок", "viewWholeHistory": "Смотреть всю историю", diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart similarity index 92% rename from frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart rename to frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart index bb618b5b..3f8f4a67 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/actions.dart @@ -4,7 +4,7 @@ import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/widgets/dialogs/payment_status_dialog.dart'; -Future handleUploadSend( +Future handleMultiplePayoutSend( BuildContext context, MultiplePayoutsController controller, ) async { diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart index d6ebe0ba..67e3294b 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/header.dart @@ -6,15 +6,12 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; class SourceQuotePanelHeader extends StatelessWidget { const SourceQuotePanelHeader({ super.key, - required this.theme, - required this.l10n, }); - final ThemeData theme; - final AppLocalizations l10n; - @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; return Text( l10n.sourceOfFunds, style: theme.textTheme.titleSmall?.copyWith( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart index f3141ec9..8bfb6b32 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart @@ -3,32 +3,29 @@ import 'package:flutter/material.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pweb/controllers/multiple_payouts.dart'; +import 'package:pweb/pages/dashboard/payouts/multiple/actions.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/header.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/summary.dart'; -import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/widgets/quote_status.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; - - class SourceQuotePanel extends StatelessWidget { const SourceQuotePanel({ super.key, required this.controller, required this.walletsController, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; final WalletsController walletsController; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; return Container( width: double.infinity, padding: const EdgeInsets.all(12), @@ -40,13 +37,11 @@ class SourceQuotePanel extends StatelessWidget { child: Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - SourceQuotePanelHeader(theme: theme, l10n: l10n), + SourceQuotePanelHeader(), const SizedBox(height: 8), SourceWalletSelector( - controller: controller, walletsController: walletsController, - theme: theme, - l10n: l10n, + isBusy: controller.isBusy, ), const SizedBox(height: 12), const Divider(height: 1), @@ -54,6 +49,26 @@ class SourceQuotePanel extends StatelessWidget { SourceQuoteSummary(controller: controller, spacing: 12), const SizedBox(height: 12), MultipleQuoteStatusCard(controller: controller), + const SizedBox(height: 12), + Center( + child: ElevatedButton( + onPressed: controller.canSend + ? () => handleMultiplePayoutSend(context, controller) + : null, + style: ElevatedButton.styleFrom( + backgroundColor: theme.colorScheme.primary, + foregroundColor: theme.colorScheme.onPrimary, + padding: const EdgeInsets.symmetric( + horizontal: 32, + vertical: 16, + ), + textStyle: theme.textTheme.titleSmall?.copyWith( + fontWeight: FontWeight.w600, + ), + ), + child: Text(l10n.send), + ), + ), ], ), ); diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart index 9911d6a2..95742f7a 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart @@ -9,49 +9,26 @@ class UploadPanelActions extends StatelessWidget { const UploadPanelActions({ super.key, required this.controller, - required this.l10n, - required this.onSend, }); - + final MultiplePayoutsController controller; - final AppLocalizations l10n; - final VoidCallback onSend; - - static const double _buttonVerticalPadding = 12; - static const double _buttonHorizontalPadding = 24; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; final hasFile = controller.selectedFileName != null; - return Wrap( - spacing: 8, - runSpacing: 8, - alignment: WrapAlignment.center, - children: [ - OutlinedButton( - onPressed: !hasFile || controller.isBusy - ? null - : () => controller.removeUploadedFile(), - style: OutlinedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: _buttonHorizontalPadding, - vertical: _buttonVerticalPadding, - ), - ), - child: Text(l10n.cancel), + if (!hasFile) return const SizedBox.shrink(); + + return TextButton( + onPressed: controller.isBusy ? null : () => controller.pickAndQuote(), + style: TextButton.styleFrom( + padding: EdgeInsets.symmetric( + horizontal: 24, + vertical: 12, ), - ElevatedButton( - onPressed: controller.canSend ? onSend : null, - style: ElevatedButton.styleFrom( - padding: const EdgeInsets.symmetric( - horizontal: _buttonHorizontalPadding, - vertical: _buttonVerticalPadding, - ), - ), - child: Text(l10n.send), - ), - ], + ), + child: Text(l10n.changeFile), ); } } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart index 5bb0629f..24b4f7bf 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart @@ -1,5 +1,7 @@ import 'package:flutter/material.dart'; +import 'package:dotted_border/dotted_border.dart'; + import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -9,100 +11,88 @@ class UploadDropZone extends StatelessWidget { const UploadDropZone({ super.key, required this.controller, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; - final ThemeData theme; - final AppLocalizations l10n; static const double _panelRadius = 12; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; final hasFile = controller.selectedFileName != null; + final borderRadius = BorderRadius.circular(_panelRadius); + final borderColor = theme.colorScheme.outlineVariant.withValues( + alpha: hasFile ? 0.55 : 1, + ); + final surfaceColor = theme.colorScheme.surfaceContainerHighest.withValues( + alpha: hasFile ? 0.2 : 0.35, + ); + final iconColor = hasFile + ? theme.colorScheme.onSurfaceVariant + : theme.colorScheme.primary; - return InkWell( - onTap: controller.isBusy - ? null - : () => controller.pickAndQuote(), - borderRadius: BorderRadius.circular(_panelRadius), - child: Container( - width: double.infinity, - padding: const EdgeInsets.symmetric(horizontal: 18, vertical: 16), - decoration: BoxDecoration( - color: theme.colorScheme.surfaceContainerHighest.withValues( - alpha: 0.5, - ), - border: Border.all(color: theme.colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(_panelRadius), + return ClipRRect( + borderRadius: borderRadius, + child: DottedBorder( + options: RoundedRectDottedBorderOptions( + radius: Radius.circular(_panelRadius), + dashPattern: const [8, 5], + strokeWidth: 1.4, + color: borderColor, ), - child: Column( - children: [ - Icon( - Icons.upload_file, - size: 34, - color: theme.colorScheme.primary, - ), - const SizedBox(height: 8), - Text( - hasFile ? controller.selectedFileName! : l10n.uploadCSV, - style: theme.textTheme.bodyMedium?.copyWith( - fontWeight: FontWeight.w600, - ), - textAlign: TextAlign.center, - ), - if (!hasFile) ...[ - const SizedBox(height: 8), - Container( + child: Material( + color: surfaceColor, + child: InkWell( + onTap: controller.isBusy + ? null + : () => controller.pickAndQuote(), + borderRadius: borderRadius, + child: SizedBox( + width: double.infinity, + child: Padding( padding: const EdgeInsets.symmetric( - horizontal: 14, - vertical: 8, + horizontal: 18, + vertical: 16, ), - decoration: BoxDecoration( - color: theme.colorScheme.primary.withValues(alpha: 0.12), - border: Border.all( - color: theme.colorScheme.primary.withValues(alpha: 0.5), - ), - borderRadius: BorderRadius.circular(999), - ), - child: Row( - mainAxisSize: MainAxisSize.min, + child: Column( children: [ Icon( - Icons.touch_app, - size: 16, - color: theme.colorScheme.primary, + Icons.upload_file, + size: 34, + color: iconColor, ), - const SizedBox(width: 6), + const SizedBox(height: 8), Text( - l10n.upload, - style: theme.textTheme.labelLarge?.copyWith( - color: theme.colorScheme.primary, + hasFile + ? controller.selectedFileName! + : l10n.uploadCSV, + style: theme.textTheme.bodyMedium?.copyWith( fontWeight: FontWeight.w600, ), + textAlign: TextAlign.center, ), + const SizedBox(height: 6), + Text( + l10n.hintUpload, + style: theme.textTheme.bodySmall, + textAlign: TextAlign.center, + ), + if (hasFile) ...[ + const SizedBox(height: 6), + Text( + '${l10n.payout}: ${controller.rows.length}', + style: theme.textTheme.labelMedium?.copyWith( + color: theme.colorScheme.onSurfaceVariant, + ), + ), + ], ], ), ), - ], - const SizedBox(height: 6), - Text( - l10n.hintUpload, - style: theme.textTheme.bodySmall, - textAlign: TextAlign.center, ), - if (hasFile) ...[ - const SizedBox(height: 6), - Text( - '${l10n.payout}: ${controller.rows.length}', - style: theme.textTheme.labelMedium?.copyWith( - color: theme.colorScheme.primary, - ), - ), - ], - ], + ), ), ), ); diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart index 10c9deac..a89dc913 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart @@ -5,14 +5,13 @@ class UploadQuoteProgress extends StatelessWidget { const UploadQuoteProgress({ super.key, required this.isQuoting, - required this.theme, }); final bool isQuoting; - final ThemeData theme; @override Widget build(BuildContext context) { + final theme = Theme.of(context); if (!isQuoting) return const SizedBox.shrink(); return Column( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart index 864df391..59cdd05a 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart @@ -9,16 +9,15 @@ class UploadPanelStatus extends StatelessWidget { const UploadPanelStatus({ super.key, required this.controller, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; + if (controller.sentCount <= 0 && controller.error == null) { return const SizedBox.shrink(); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart index 380c2e9a..613a611e 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart @@ -4,40 +4,32 @@ import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/drop_zone.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/actions.dart'; -import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/helpers.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/status.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/progress.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class UploadPanel extends StatelessWidget { const UploadPanel({ super.key, required this.controller, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final hasFile = controller.selectedFileName != null; return Column( mainAxisAlignment: MainAxisAlignment.center, crossAxisAlignment: CrossAxisAlignment.center, children: [ - UploadDropZone(controller: controller, theme: theme, l10n: l10n), - UploadQuoteProgress(isQuoting: controller.isQuoting, theme: theme), - const SizedBox(height: 12), - UploadPanelActions( - controller: controller, - l10n: l10n, - onSend: () => handleUploadSend(context, controller), - ), - UploadPanelStatus(controller: controller, theme: theme, l10n: l10n), + UploadDropZone(controller: controller), + UploadQuoteProgress(isQuoting: controller.isQuoting), + if (hasFile) ...[ + const SizedBox(height: 12), + UploadPanelActions(controller: controller), + ], + UploadPanelStatus(controller: controller), ], ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart index 7844d6b8..41dda609 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/sample/table.dart @@ -24,7 +24,7 @@ class FileFormatSampleTable extends StatelessWidget { DataColumn(label: Text(l10n.firstName)), DataColumn(label: Text(l10n.lastName)), DataColumn(label: Text(l10n.expiryDate)), - DataColumn(label: Text(l10n.amount)), + DataColumn(label: Text(l10n.amountColumn)), ], rows: rows.map((row) { return DataRow( diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart index 55d9d870..db28f61b 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart @@ -2,20 +2,19 @@ import 'package:flutter/material.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; + class UploadCsvHeader extends StatelessWidget { const UploadCsvHeader({ super.key, required this.theme, - required this.l10n, }); final ThemeData theme; - final AppLocalizations l10n; - static const double _iconTextSpacing = 5; @override Widget build(BuildContext context) { + final l10n = AppLocalizations.of(context)!; return Row( children: [ const Icon(Icons.upload), diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart index b9e8e761..61598de7 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart @@ -3,11 +3,8 @@ import 'package:flutter/material.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pweb/controllers/multiple_payouts.dart'; - import 'package:pweb/pages/dashboard/payouts/multiple/panels/source_quote/widget.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/panels/upload_panel/widget.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart'; @@ -16,17 +13,14 @@ class UploadCsvLayout extends StatelessWidget { super.key, required this.controller, required this.walletsController, - required this.theme, - required this.l10n, }); final MultiplePayoutsController controller; final WalletsController walletsController; - final ThemeData theme; - final AppLocalizations l10n; @override Widget build(BuildContext context) { + final hasFile = controller.selectedFileName != null; return LayoutBuilder( builder: (context, constraints) { final useHorizontal = constraints.maxWidth >= 760; @@ -34,24 +28,29 @@ class UploadCsvLayout extends StatelessWidget { return Column( children: [ PanelCard( - theme: theme, child: UploadPanel( controller: controller, - theme: theme, - l10n: l10n, ), ), - const SizedBox(height: 12), - SourceQuotePanel( - controller: controller, - walletsController: walletsController, - theme: theme, - l10n: l10n, - ), + if (hasFile) ...[ + const SizedBox(height: 12), + SourceQuotePanel( + controller: controller, + walletsController: walletsController, + ), + ], ], ); } + if (!hasFile) { + return PanelCard( + child: UploadPanel( + controller: controller, + ), + ); + } + return IntrinsicHeight( child: Row( crossAxisAlignment: CrossAxisAlignment.stretch, @@ -59,11 +58,8 @@ class UploadCsvLayout extends StatelessWidget { Expanded( flex: 3, child: PanelCard( - theme: theme, child: UploadPanel( controller: controller, - theme: theme, - l10n: l10n, ), ), ), @@ -73,8 +69,6 @@ class UploadCsvLayout extends StatelessWidget { child: SourceQuotePanel( controller: controller, walletsController: walletsController, - theme: theme, - l10n: l10n, ), ), ], diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart index e922bf69..ab19e583 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/panel_card.dart @@ -4,11 +4,9 @@ import 'package:flutter/material.dart'; class PanelCard extends StatelessWidget { const PanelCard({ super.key, - required this.theme, required this.child, }); - final ThemeData theme; final Widget child; @override @@ -16,11 +14,6 @@ class PanelCard extends StatelessWidget { return Container( width: double.infinity, padding: const EdgeInsets.all(16), - decoration: BoxDecoration( - color: theme.colorScheme.surface, - border: Border.all(color: theme.colorScheme.outlineVariant), - borderRadius: BorderRadius.circular(8), - ), child: child, ); } diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart index a30f33cf..fd1f51cf 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart +++ b/frontend/pweb/lib/pages/dashboard/payouts/multiple/sections/upload_csv/widget.dart @@ -6,8 +6,6 @@ import 'package:pweb/controllers/multiple_payouts.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/header.dart'; import 'package:pweb/pages/dashboard/payouts/multiple/sections/upload_csv/layout.dart'; -import 'package:pweb/generated/i18n/app_localizations.dart'; - class UploadCSVSection extends StatelessWidget { const UploadCSVSection({super.key}); @@ -18,18 +16,15 @@ class UploadCSVSection extends StatelessWidget { Widget build(BuildContext context) { final controller = context.watch(); final theme = Theme.of(context); - final l10n = AppLocalizations.of(context)!; return Column( crossAxisAlignment: CrossAxisAlignment.start, children: [ - UploadCsvHeader(theme: theme, l10n: l10n), + UploadCsvHeader(theme: theme), const SizedBox(height: _verticalSpacing), UploadCsvLayout( controller: controller, walletsController: context.watch(), - theme: theme, - l10n: l10n, ), ], ); diff --git a/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart b/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart index 3d6fdc08..829182a4 100644 --- a/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart +++ b/frontend/pweb/lib/pages/payment_methods/payment_page/method_selector.dart @@ -5,7 +5,7 @@ import 'package:provider/provider.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; import 'package:pshared/models/payment/wallet.dart'; -import 'package:pweb/utils/payment/dropdown.dart'; +import 'package:pweb/widgets/payment/source_wallet_selector.dart'; class PaymentMethodSelector extends StatelessWidget { @@ -18,9 +18,8 @@ class PaymentMethodSelector extends StatelessWidget { @override Widget build(BuildContext context) => Consumer( - builder: (context, provider, _) => PaymentMethodDropdown( - methods: provider.wallets, - selectedMethod: provider.selectedWallet, + builder: (context, provider, _) => SourceWalletSelector( + walletsController: provider, onChanged: onMethodChanged, ), ); diff --git a/frontend/pweb/lib/services/payments/csv_input.dart b/frontend/pweb/lib/services/payments/csv_input.dart index eb70e733..0d6c2116 100644 --- a/frontend/pweb/lib/services/payments/csv_input.dart +++ b/frontend/pweb/lib/services/payments/csv_input.dart @@ -19,17 +19,11 @@ class WebCsvInputService implements CsvInputService { Future pickCsv() async { final input = html.FileUploadInputElement() ..accept = '.csv,text/csv' - ..multiple = false; + ..multiple = false + ..style.display = 'none'; - final completer = Completer(); - input.onChange.listen((_) { - completer.complete( - input.files?.isNotEmpty == true ? input.files!.first : null, - ); - }); - input.click(); - - final file = await completer.future; + html.document.body?.append(input); + final file = await _pickFile(input); if (file == null) return null; final reader = html.FileReader(); @@ -50,4 +44,52 @@ class WebCsvInputService implements CsvInputService { final content = await readCompleter.future; return PickedCsvFile(name: file.name, content: content); } + + Future _pickFile(html.FileUploadInputElement input) async { + final completer = Completer(); + + void completeWith(html.File? file) { + if (!completer.isCompleted) completer.complete(file); + } + + StreamSubscription? changeSub; + StreamSubscription? inputSub; + StreamSubscription? focusSub; + Timer? timeout; + + void cleanup() { + changeSub?.cancel(); + inputSub?.cancel(); + focusSub?.cancel(); + timeout?.cancel(); + input.remove(); + } + + void tryComplete() { + final file = input.files?.isNotEmpty == true ? input.files!.first : null; + if (file != null) { + completeWith(file); + } + } + + changeSub = input.onChange.listen((_) => tryComplete()); + inputSub = input.onInput.listen((_) => tryComplete()); + focusSub = html.window.onFocus.listen((_) async { + if (completer.isCompleted) return; + for (var i = 0; i < 6; i++) { + tryComplete(); + if (completer.isCompleted) return; + await Future.delayed(const Duration(milliseconds: 120)); + } + if (!completer.isCompleted && (input.value ?? '').isEmpty) { + completeWith(null); + } + }); + + timeout = Timer(const Duration(minutes: 2), () => completeWith(null)); + + completer.future.whenComplete(cleanup); + input.click(); + return completer.future; + } } diff --git a/frontend/pweb/lib/utils/payment/dropdown.dart b/frontend/pweb/lib/utils/payment/dropdown.dart deleted file mode 100644 index 291b30c8..00000000 --- a/frontend/pweb/lib/utils/payment/dropdown.dart +++ /dev/null @@ -1,53 +0,0 @@ -import 'package:flutter/material.dart'; - -import 'package:pshared/models/payment/type.dart'; -import 'package:pshared/models/payment/wallet.dart'; - -import 'package:pweb/pages/payment_methods/icon.dart'; - -import 'package:pweb/generated/i18n/app_localizations.dart'; - - -class PaymentMethodDropdown extends StatelessWidget { - final List methods; - final ValueChanged onChanged; - final Wallet? selectedMethod; - - const PaymentMethodDropdown({ - super.key, - required this.methods, - required this.onChanged, - this.selectedMethod, - }); - - @override - Widget build(BuildContext context) => DropdownButtonFormField( - dropdownColor: Theme.of(context).colorScheme.onSecondary, - initialValue: _getSelectedMethod(), - decoration: InputDecoration( - labelText: AppLocalizations.of(context)!.whereGetMoney, - border: OutlineInputBorder(borderRadius: BorderRadius.circular(8)), - ), - items: methods.map((method) => DropdownMenuItem( - value: method, - child: Row( - children: [ - Icon(iconForPaymentType(PaymentType.managedWallet), size: 20), - const SizedBox(width: 8), - Text(method.name), - ], - ), - )).toList(), - onChanged: (value) { - if (value != null) { - onChanged(value); - } - }, - ); - - Wallet? _getSelectedMethod() { - if (selectedMethod != null) return selectedMethod; - if (methods.isEmpty) return null; - return methods.first; - } -} diff --git a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart b/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart similarity index 55% rename from frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart rename to frontend/pweb/lib/widgets/payment/source_wallet_selector.dart index 0f89dd13..a004bc77 100644 --- a/frontend/pweb/lib/pages/dashboard/payouts/multiple/panels/source_quote/selector.dart +++ b/frontend/pweb/lib/widgets/payment/source_wallet_selector.dart @@ -1,31 +1,30 @@ import 'package:flutter/material.dart'; import 'package:pshared/controllers/balance_mask/wallets.dart'; +import 'package:pshared/models/payment/wallet.dart'; import 'package:pshared/utils/currency.dart'; -import 'package:pweb/controllers/multiple_payouts.dart'; - import 'package:pweb/generated/i18n/app_localizations.dart'; class SourceWalletSelector extends StatelessWidget { const SourceWalletSelector({ super.key, - required this.controller, required this.walletsController, - required this.theme, - required this.l10n, + this.isBusy = false, + this.onChanged, }); - final MultiplePayoutsController controller; final WalletsController walletsController; - final ThemeData theme; - final AppLocalizations l10n; + final bool isBusy; + final ValueChanged? onChanged; @override Widget build(BuildContext context) { final wallets = walletsController.wallets; final selectedWalletRef = walletsController.selectedWalletRef; + final theme = Theme.of(context); + final l10n = AppLocalizations.of(context)!; if (wallets.isEmpty) { return Text(l10n.noWalletsAvailable, style: theme.textTheme.bodySmall); @@ -47,18 +46,43 @@ class SourceWalletSelector extends StatelessWidget { (wallet) => DropdownMenuItem( value: wallet.id, child: Text( - '${wallet.name} - ${amountToString(wallet.balance)} ${currencyCodeToString(wallet.currency)}', + '${_walletLabel(wallet)} - ${currencyCodeToSymbol(wallet.currency)} ${amountToString(wallet.balance)}', overflow: TextOverflow.ellipsis, ), ), ) .toList(growable: false), - onChanged: controller.isBusy + onChanged: isBusy ? null : (value) { if (value == null) return; walletsController.selectWalletByRef(value); + final selected = walletsController.selectedWallet; + if (selected != null) { + onChanged?.call(selected); + } }, ); } + + String _walletLabel(Wallet wallet) { + final description = wallet.description?.trim(); + if (description != null && description.isNotEmpty) { + return description; + } + final name = wallet.name.trim(); + if (name.isNotEmpty && !_looksLikeId(name)) { + return name; + } + final token = wallet.tokenSymbol?.trim(); + if (token != null && token.isNotEmpty) { + return '$token wallet'; + } + return '${currencyCodeToString(wallet.currency)} wallet'; + } + + bool _looksLikeId(String value) { + return RegExp(r'^[a-f0-9]{12,}$', caseSensitive: false) + .hasMatch(value); + } } -- 2.49.1 From fcbfa323c85d3acb71fb51d2227568efbc8a34a4 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 20:26:10 +0100 Subject: [PATCH 09/21] fixed verificatoin --- .../internal/mongo/verificationimp/create.go | 12 ++++ .../verificationimp/verification_test.go | 65 +++++++++++++++++++ 2 files changed, 77 insertions(+) diff --git a/api/pkg/db/internal/mongo/verificationimp/create.go b/api/pkg/db/internal/mongo/verificationimp/create.go index c4327cbc..f627cf26 100644 --- a/api/pkg/db/internal/mongo/verificationimp/create.go +++ b/api/pkg/db/internal/mongo/verificationimp/create.go @@ -11,6 +11,7 @@ import ( "github.com/tech/sendico/pkg/db/verification" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/model" + "go.mongodb.org/mongo-driver/v2/bson" ) func normalizedIdempotencyKey(value *string) (string, bool) { @@ -24,6 +25,10 @@ func normalizedIdempotencyKey(value *string) (string, bool) { return key, true } +func syntheticIdempotencyKey() string { + return "auto:" + bson.NewObjectID().Hex() +} + func idempotencyFilter( request *verification.Request, idempotencyKey string, @@ -119,6 +124,13 @@ func (db *verificationDB) Create( } idempotencyKey, hasIdempotency := normalizedIdempotencyKey(request.IdempotencyKey) + if !hasIdempotency { + // Legacy deployments may still enforce uniqueness on (accountRef, purpose, target, idempotencyKey), + // where missing idempotency key behaves like a shared null key. Assign an internal per-request key + // so token reissue works even when callers do not provide idempotency explicitly. + idempotencyKey = syntheticIdempotencyKey() + hasIdempotency = true + } token, raw, err := newVerificationToken(request, idempotencyKey, hasIdempotency) if err != nil { diff --git a/api/pkg/db/internal/mongo/verificationimp/verification_test.go b/api/pkg/db/internal/mongo/verificationimp/verification_test.go index e1312a88..364c87c9 100644 --- a/api/pkg/db/internal/mongo/verificationimp/verification_test.go +++ b/api/pkg/db/internal/mongo/verificationimp/verification_test.go @@ -3,6 +3,7 @@ package verificationimp import ( "context" "errors" + "strings" "sync" "testing" "time" @@ -101,6 +102,27 @@ func (m *memoryTokenRepository) Insert(_ context.Context, obj storable.Storable, if _, exists := m.data[*id]; exists { return merrors.DataConflict("token already exists") } + for _, existing := range m.data { + if existing.VerifyTokenHash == tok.VerifyTokenHash { + return merrors.DataConflict("duplicate verifyTokenHash") + } + if existing.AccountRef != tok.AccountRef { + continue + } + if existing.Purpose != tok.Purpose { + continue + } + if existing.Target != tok.Target { + continue + } + + switch { + case existing.IdempotencyKey == nil && tok.IdempotencyKey == nil: + return merrors.DataConflict("duplicate verification context idempotency") + case existing.IdempotencyKey != nil && tok.IdempotencyKey != nil && *existing.IdempotencyKey == *tok.IdempotencyKey: + return merrors.DataConflict("duplicate verification context idempotency") + } + } m.data[*id] = cloneToken(tok) m.order = append(m.order, *id) return nil @@ -633,6 +655,49 @@ func TestCreate_InvalidatesPreviousToken(t *testing.T) { assert.Equal(t, accountRef, tok.AccountRef) } +func TestCreate_AccountActivationResendWithoutIdempotency_ReissuesToken(t *testing.T) { + db := newTestVerificationDB(t) + ctx := context.Background() + accountRef := bson.NewObjectID() + + // First issue during signup. + firstRaw, err := db.Create(ctx, req(accountRef, model.PurposeAccountActivation, "", time.Hour)) + require.NoError(t, err) + + // Second issue during resend should rotate token instead of failing with duplicate key. + secondRaw, err := db.Create(ctx, req(accountRef, model.PurposeAccountActivation, "", time.Hour)) + require.NoError(t, err) + assert.NotEqual(t, firstRaw, secondRaw) + + // Old token becomes unusable after reissue. + _, err = db.Consume(ctx, bson.NilObjectID, model.PurposeAccountActivation, firstRaw) + require.Error(t, err) + assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed)) + + // New token is valid. + tok, err := db.Consume(ctx, bson.NilObjectID, model.PurposeAccountActivation, secondRaw) + require.NoError(t, err) + assert.Equal(t, accountRef, tok.AccountRef) + assert.Equal(t, model.PurposeAccountActivation, tok.Purpose) + + // Non-idempotent requests should still persist unique internal keys, + // preventing uniqueness collisions on (accountRef, purpose, target, idempotencyKey). + repo := db.Repository.(*memoryTokenRepository) + repo.mu.Lock() + defer repo.mu.Unlock() + + keys := map[string]struct{}{} + for _, stored := range repo.data { + if stored.AccountRef != accountRef || stored.Purpose != model.PurposeAccountActivation { + continue + } + require.NotNil(t, stored.IdempotencyKey) + assert.True(t, strings.HasPrefix(*stored.IdempotencyKey, "auto:")) + keys[*stored.IdempotencyKey] = struct{}{} + } + assert.Len(t, keys, 2) +} + func TestCreate_InvalidatesMultiplePreviousTokens(t *testing.T) { db := newTestVerificationDB(t) ctx := context.Background() -- 2.49.1 From e605c734ad20670700bcee5bba2d7b067ad052ac Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 20:46:11 +0100 Subject: [PATCH 10/21] fixed verificaiton error --- .../internal/mongo/verificationimp/create.go | 45 +++++++++++++------ .../verificationimp/verification_test.go | 15 ++++++- 2 files changed, 45 insertions(+), 15 deletions(-) diff --git a/api/pkg/db/internal/mongo/verificationimp/create.go b/api/pkg/db/internal/mongo/verificationimp/create.go index f627cf26..13f15948 100644 --- a/api/pkg/db/internal/mongo/verificationimp/create.go +++ b/api/pkg/db/internal/mongo/verificationimp/create.go @@ -29,6 +29,35 @@ func syntheticIdempotencyKey() string { return "auto:" + bson.NewObjectID().Hex() } +func verificationContextFilter(request *verification.Request) builder.Query { + return repository.Query().And( + repository.Filter("accountRef", request.AccountRef), + repository.Filter("purpose", request.Purpose), + repository.Filter("target", request.Target), + ) +} + +func activeContextFilter(request *verification.Request, now time.Time) builder.Query { + return repository.Query().And( + repository.Filter("accountRef", request.AccountRef), + repository.Filter("purpose", request.Purpose), + repository.Filter("target", request.Target), + repository.Filter("usedAt", nil), + repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), + ) +} + +func cooldownActiveContextFilter(request *verification.Request, now, cutoff time.Time) builder.Query { + return repository.Query().And( + repository.Filter("accountRef", request.AccountRef), + repository.Filter("purpose", request.Purpose), + repository.Filter("target", request.Target), + repository.Filter("usedAt", nil), + repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), + repository.Query().Comparison(repository.Field("createdAt"), builder.Gt, cutoff), + ) +} + func idempotencyFilter( request *verification.Request, idempotencyKey string, @@ -140,13 +169,7 @@ func (db *verificationDB) Create( _, err = db.tf.CreateTransaction().Execute(ctx, func(tx context.Context) (any, error) { now := time.Now().UTC() - baseFilter := repository.Query().And( - repository.Filter("accountRef", request.AccountRef), - repository.Filter("purpose", request.Purpose), - repository.Filter("target", request.Target), - repository.Filter("usedAt", nil), - repository.Query().Comparison(repository.Field("expiresAt"), builder.Gt, now), - ) + activeFilter := activeContextFilter(request, now) // Optional idempotency key support for safe retries. if hasIdempotency { @@ -177,12 +200,8 @@ func (db *verificationDB) Create( if request.Cooldown != nil { cutoff := now.Add(-*request.Cooldown) - cooldownFilter := baseFilter.And( - repository.Query().Comparison(repository.Field("createdAt"), builder.Gt, cutoff), - ) - var recent model.VerificationToken - err := db.DBImp.FindOne(tx, cooldownFilter, &recent) + err := db.DBImp.FindOne(tx, cooldownActiveContextFilter(request, now, cutoff), &recent) switch { case err == nil: return nil, verification.ErrorCooldownActive() @@ -195,7 +214,7 @@ func (db *verificationDB) Create( // 2) Invalidate active tokens for this context if _, err := db.DBImp.PatchMany( tx, - baseFilter, + activeFilter, repository.Patch().Set(repository.Field("usedAt"), now), ); err != nil { return nil, err diff --git a/api/pkg/db/internal/mongo/verificationimp/verification_test.go b/api/pkg/db/internal/mongo/verificationimp/verification_test.go index 364c87c9..9220730a 100644 --- a/api/pkg/db/internal/mongo/verificationimp/verification_test.go +++ b/api/pkg/db/internal/mongo/verificationimp/verification_test.go @@ -849,15 +849,26 @@ func TestCreate_CooldownExpiresAllowsCreation(t *testing.T) { accountRef := bson.NewObjectID() // First creation without cooldown. - _, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour)) + firstRaw, err := db.Create(ctx, req(accountRef, model.PurposePasswordReset, "", time.Hour)) require.NoError(t, err) time.Sleep(2 * time.Millisecond) // Re-create with short cooldown — the prior token is old enough to be invalidated. r2 := req(accountRef, model.PurposePasswordReset, "", time.Hour).WithCooldown(time.Millisecond) - _, err = db.Create(ctx, r2) + secondRaw, err := db.Create(ctx, r2) require.NoError(t, err) + assert.NotEqual(t, firstRaw, secondRaw) + + // Old token should be rotated out after successful re-issue. + _, err = db.Consume(ctx, accountRef, model.PurposePasswordReset, firstRaw) + require.Error(t, err) + assert.True(t, errors.Is(err, verification.ErrTokenAlreadyUsed)) + + // New token remains valid. + tok, err := db.Consume(ctx, accountRef, model.PurposePasswordReset, secondRaw) + require.NoError(t, err) + assert.Equal(t, accountRef, tok.AccountRef) } func TestCreate_CooldownNilIgnored(t *testing.T) { -- 2.49.1 From a862e27087a4a56b9f491114c9dfcb58e317642e Mon Sep 17 00:00:00 2001 From: Stephan D Date: Thu, 12 Feb 2026 21:10:33 +0100 Subject: [PATCH 11/21] new payment methods service --- .woodpecker/bff.yml | 3 + .woodpecker/payments_methods.yml | 79 ++++ Makefile | 6 +- README.md | 3 +- api/billing/documents/go.mod | 2 +- api/billing/documents/go.sum | 24 +- api/billing/fees/go.mod | 2 +- api/billing/fees/go.sum | 24 +- api/discovery/go.mod | 2 +- api/discovery/go.sum | 24 +- api/fx/ingestor/go.mod | 2 +- api/fx/ingestor/go.sum | 24 +- .../internal/ingestor/service_test.go | 2 +- api/fx/oracle/go.mod | 2 +- api/fx/oracle/go.sum | 24 +- .../internal/service/oracle/service_test.go | 10 +- api/fx/storage/mongo/repository.go | 4 +- api/fx/storage/mongo/store/quotes.go | 2 +- api/gateway/chain/go.mod | 2 +- api/gateway/chain/go.sum | 24 +- api/gateway/mntx/go.mod | 2 +- api/gateway/mntx/go.sum | 24 +- api/gateway/tgsettle/go.mod | 2 +- api/gateway/tgsettle/go.sum | 24 +- api/gateway/tron/go.mod | 2 +- api/gateway/tron/go.sum | 24 +- api/ledger/go.mod | 2 +- api/ledger/go.sum | 24 +- api/notification/go.mod | 2 +- api/notification/go.sum | 24 +- api/payments/methods/.air.toml | 46 +++ api/payments/methods/client/client.go | 131 +++++++ api/payments/methods/client/config.go | 20 + api/payments/methods/config.dev.yml | 49 +++ api/payments/methods/config.yml | 49 +++ api/payments/methods/env/.gitignore | 1 + api/payments/methods/go.mod | 52 +++ api/payments/methods/go.sum | 221 ++++++++++++ .../methods/internal/appversion/version.go | 28 ++ .../internal/server/internal/config.go | 62 ++++ .../internal/server/internal/discovery.go | 88 +++++ .../internal/server/internal/lifecycle.go | 16 + .../internal/server/internal/serverimp.go | 117 ++++++ .../methods/internal/server/internal/types.go | 29 ++ .../methods/internal/server/server.go | 12 + .../internal/service/methods/archive.go | 36 ++ .../internal/service/methods/create.go | 41 +++ .../internal/service/methods/delete.go | 37 ++ .../methods/internal/service/methods/get.go | 38 ++ .../methods/internal/service/methods/list.go | 48 +++ .../service/methods/recipient_consumer.go | 87 +++++ .../internal/service/methods/service.go | 90 +++++ .../internal/service/methods/update.go | 37 ++ .../methods/internal/service/methods/util.go | 83 +++++ api/payments/methods/main.go | 17 + api/payments/orchestrator/go.mod | 4 +- api/payments/orchestrator/go.sum | 4 +- .../service/orchestrator/service_helpers.go | 3 +- .../orchestrator/service_helpers_test.go | 10 +- .../service/orchestrator/service_test.go | 8 +- api/payments/quotation/go.mod | 2 +- api/payments/quotation/go.sum | 24 +- .../service/quotation/handlers_commands.go | 7 +- .../quotation/handlers_commands_test.go | 14 +- .../service/quotation/service_helpers.go | 3 +- api/payments/storage/mongo/repository.go | 65 +++- .../storage/mongo/store/payment_methods.go | 235 ++++++++++++ api/payments/storage/storage.go | 17 +- api/pkg/auth/factory.go | 6 + api/pkg/db/factory.go | 2 - api/pkg/db/internal/mongo/db.go | 14 +- .../db/internal/mongo/paymethoddb/archived.go | 20 - api/pkg/db/internal/mongo/paymethoddb/db.go | 59 --- api/pkg/db/internal/mongo/paymethoddb/list.go | 28 -- .../db/internal/mongo/recipientdb/archived.go | 37 -- api/pkg/db/internal/mongo/recipientdb/db.go | 4 - api/pkg/db/paymethod/db.go | 15 - api/pkg/go.mod | 8 +- api/pkg/go.sum | 28 +- api/pkg/model/internal/notificationevent.go | 2 +- api/pkg/model/notificationevent.go | 1 + api/pkg/mservice/services.go | 4 +- api/proto/payments/methods/v1/methods.proto | 80 ++++ api/server/config.dev.yml | 6 + api/server/config.yml | 6 + api/server/go.mod | 5 +- api/server/go.sum | 4 +- api/server/interface/api/config.go | 1 + api/server/internal/api/discovery_resolver.go | 41 +++ .../internal/server/accountapiimp/service.go | 46 +++ .../internal/server/accountapiimp/signup.go | 59 +++ .../accountapiimp/signup_ledger_test.go | 116 ++++++ .../internal/server/paymethodsimp/service.go | 341 +++++++++++++++++- .../server/recipientimp/notifications.go | 22 ++ .../internal/server/recipientimp/service.go | 6 +- ci/dev/bff.dockerfile | 2 + ci/dev/payments-methods.dockerfile | 40 ++ ci/prod/.env.runtime | 7 + ci/prod/compose/bff.yml | 1 + ci/prod/compose/payments_methods.dockerfile | 40 ++ ci/prod/compose/payments_methods.yml | 53 +++ ci/prod/scripts/deploy/payments_methods.sh | 147 ++++++++ ci/scripts/payments_methods/build-image.sh | 85 +++++ ci/scripts/payments_methods/deploy.sh | 59 +++ ci/scripts/proto/generate.sh | 6 + docker-compose.dev.yml | 49 +++ 106 files changed, 3262 insertions(+), 414 deletions(-) create mode 100644 .woodpecker/payments_methods.yml create mode 100644 api/payments/methods/.air.toml create mode 100644 api/payments/methods/client/client.go create mode 100644 api/payments/methods/client/config.go create mode 100644 api/payments/methods/config.dev.yml create mode 100644 api/payments/methods/config.yml create mode 100644 api/payments/methods/env/.gitignore create mode 100644 api/payments/methods/go.mod create mode 100644 api/payments/methods/go.sum create mode 100644 api/payments/methods/internal/appversion/version.go create mode 100644 api/payments/methods/internal/server/internal/config.go create mode 100644 api/payments/methods/internal/server/internal/discovery.go create mode 100644 api/payments/methods/internal/server/internal/lifecycle.go create mode 100644 api/payments/methods/internal/server/internal/serverimp.go create mode 100644 api/payments/methods/internal/server/internal/types.go create mode 100644 api/payments/methods/internal/server/server.go create mode 100644 api/payments/methods/internal/service/methods/archive.go create mode 100644 api/payments/methods/internal/service/methods/create.go create mode 100644 api/payments/methods/internal/service/methods/delete.go create mode 100644 api/payments/methods/internal/service/methods/get.go create mode 100644 api/payments/methods/internal/service/methods/list.go create mode 100644 api/payments/methods/internal/service/methods/recipient_consumer.go create mode 100644 api/payments/methods/internal/service/methods/service.go create mode 100644 api/payments/methods/internal/service/methods/update.go create mode 100644 api/payments/methods/internal/service/methods/util.go create mode 100644 api/payments/methods/main.go create mode 100644 api/payments/storage/mongo/store/payment_methods.go delete mode 100644 api/pkg/db/internal/mongo/paymethoddb/archived.go delete mode 100644 api/pkg/db/internal/mongo/paymethoddb/db.go delete mode 100644 api/pkg/db/internal/mongo/paymethoddb/list.go delete mode 100644 api/pkg/db/paymethod/db.go create mode 100644 api/proto/payments/methods/v1/methods.proto create mode 100644 api/server/internal/server/accountapiimp/signup_ledger_test.go create mode 100644 api/server/internal/server/recipientimp/notifications.go create mode 100644 ci/dev/payments-methods.dockerfile create mode 100644 ci/prod/compose/payments_methods.dockerfile create mode 100644 ci/prod/compose/payments_methods.yml create mode 100755 ci/prod/scripts/deploy/payments_methods.sh create mode 100755 ci/scripts/payments_methods/build-image.sh create mode 100755 ci/scripts/payments_methods/deploy.sh diff --git a/.woodpecker/bff.yml b/.woodpecker/bff.yml index 25c29c7a..63cef64a 100644 --- a/.woodpecker/bff.yml +++ b/.woodpecker/bff.yml @@ -12,6 +12,9 @@ when: path: include: - api/server/** + - api/payments/methods/client/** + - api/payments/methods/go.mod + - api/payments/methods/go.sum - api/proto/** - api/pkg/** ignore_message: '[rebuild]' diff --git a/.woodpecker/payments_methods.yml b/.woodpecker/payments_methods.yml new file mode 100644 index 00000000..7c237649 --- /dev/null +++ b/.woodpecker/payments_methods.yml @@ -0,0 +1,79 @@ +matrix: + include: + - PAYMENTS_METHODS_IMAGE_PATH: payments/methods + PAYMENTS_METHODS_DOCKERFILE: ci/prod/compose/payments_methods.dockerfile + PAYMENTS_METHODS_MONGO_SECRET_PATH: sendico/db + PAYMENTS_METHODS_ENV: prod + +when: + - event: push + branch: main + path: + include: + - api/payments/methods/** + - api/proto/** + - api/pkg/** + ignore_message: '[rebuild]' + +steps: + - name: version + image: alpine:latest + commands: + - set -euo pipefail 2>/dev/null || set -eu + - apk add --no-cache git + - GIT_REV="$(git rev-parse --short HEAD)" + - BUILD_BRANCH="$(git rev-parse --abbrev-ref HEAD)" + - APP_V="$(cat version)" + - BUILD_DATE="$(date -u +%Y-%m-%dT%H:%M:%SZ)" + - BUILD_USER="${WOODPECKER_MACHINE:-woodpecker}" + - printf "GIT_REV=%s\nBUILD_BRANCH=%s\nAPP_V=%s\nBUILD_DATE=%s\nBUILD_USER=%s\n" \ + "$GIT_REV" "$BUILD_BRANCH" "$APP_V" "$BUILD_DATE" "$BUILD_USER" | tee .env.version + + - name: proto + image: golang:alpine + depends_on: [ version ] + commands: + - set -eu + - apk add --no-cache bash git build-base protoc protobuf-dev + - go install google.golang.org/protobuf/cmd/protoc-gen-go@latest + - go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest + - export PATH="$(go env GOPATH)/bin:$PATH" + - bash ci/scripts/proto/generate.sh + + - name: secrets + image: alpine:latest + depends_on: [ version ] + environment: + VAULT_ADDR: { from_secret: VAULT_ADDR } + VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE } + VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID } + commands: + - set -euo pipefail + - apk add --no-cache bash coreutils openssh-keygen curl sed python3 + - mkdir -p secrets + - ./ci/vlt kv_to_file kv ops/deploy/ssh_key private_b64 secrets/SSH_KEY.b64 600 + - base64 -d secrets/SSH_KEY.b64 > secrets/SSH_KEY + - chmod 600 secrets/SSH_KEY + - ssh-keygen -y -f secrets/SSH_KEY >/dev/null + - ./ci/vlt kv_get kv registry user > secrets/REGISTRY_USER + - ./ci/vlt kv_get kv registry password > secrets/REGISTRY_PASSWORD + + - name: build-image + image: gcr.io/kaniko-project/executor:debug + depends_on: [ proto, secrets ] + commands: + - sh ci/scripts/payments_methods/build-image.sh + + - name: deploy + image: alpine:latest + depends_on: [ secrets, build-image ] + environment: + VAULT_ADDR: { from_secret: VAULT_ADDR } + VAULT_ROLE_ID: { from_secret: VAULT_APP_ROLE } + VAULT_SECRET_ID: { from_secret: VAULT_SECRET_ID } + commands: + - set -euo pipefail + - apk add --no-cache bash openssh-client rsync coreutils curl sed python3 + - mkdir -p /root/.ssh + - install -m 600 secrets/SSH_KEY /root/.ssh/id_rsa + - sh ci/scripts/payments_methods/deploy.sh diff --git a/Makefile b/Makefile index 6ec216d7..8c5a17ab 100644 --- a/Makefile +++ b/Makefile @@ -218,6 +218,8 @@ services-up: dev-billing-documents \ dev-ledger \ dev-payments-orchestrator \ + dev-payments-quotation \ + dev-payments-methods \ dev-chain-gateway \ dev-tron-gateway \ dev-mntx-gateway \ @@ -245,6 +247,8 @@ list-services: @echo " - dev-billing-documents :50061, :9409 (Billing Documents)" @echo " - dev-ledger :50052, :9401 (Double-Entry Ledger)" @echo " - dev-payments-orchestrator :50062, :9403 (Payment Orchestration)" + @echo " - dev-payments-quotation :50064, :9414 (Payment Quotation)" + @echo " - dev-payments-methods :50066, :9416 (Payment Methods)" @echo " - dev-chain-gateway :50070, :9404 (EVM Blockchain Gateway)" @echo " - dev-tron-gateway :50071, :9408 (TRON Blockchain Gateway)" @echo " - dev-mntx-gateway :50075, :9405, :8084 (Card Payouts)" @@ -273,7 +277,7 @@ build-fx: build-payments: @echo "$(GREEN)Building payment services...$(NC)" - @$(COMPOSE) build dev-payments-orchestrator + @$(COMPOSE) build dev-payments-orchestrator dev-payments-quotation dev-payments-methods build-gateways: @echo "$(GREEN)Building gateway services...$(NC)" diff --git a/README.md b/README.md index 6b2175fc..31d33598 100644 --- a/README.md +++ b/README.md @@ -17,6 +17,7 @@ Financial services platform providing payment orchestration, ledger accounting, | Ledger | `api/ledger/` | Double-entry accounting | | Orchestrator | `api/payments/orchestrator/` | Payment orchestration | | Quotation | `api/payments/quotation/` | Payment quotation | +| Payment Methods | `api/payments/methods/` | Payment methods | | Billing Fees | `api/billing/fees/` | Fee calculation | | Billing Documents | `api/billing/documents/` | Billing documents | | FX Oracle | `api/fx/oracle/` | FX quote provider | @@ -24,7 +25,7 @@ Financial services platform providing payment orchestration, ledger accounting, | Gateway Chain | `api/gateway/chain/` | EVM blockchain gateway | | Gateway TRON | `api/gateway/tron/` | TRON blockchain gateway | | Gateway MNTX | `api/gateway/mntx/` | Card payouts | -| Gateway TGSettle | `api/gateway/tgsettle/` | Settlements | +| Gateway TGSettle | `api/gateway/tgsettle/` | Telegram settlements with MNTX | | Notification | `api/notification/` | Notifications | | BFF | `api/server/` | Backend for frontend | | Frontend | `frontend/pweb/` | Flutter web UI | diff --git a/api/billing/documents/go.mod b/api/billing/documents/go.mod index 347d3098..23467825 100644 --- a/api/billing/documents/go.mod +++ b/api/billing/documents/go.mod @@ -15,7 +15,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/billing/documents/go.sum b/api/billing/documents/go.sum index 3bd6cadc..b2fa29f2 100644 --- a/api/billing/documents/go.sum +++ b/api/billing/documents/go.sum @@ -201,16 +201,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -260,8 +260,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 6096ab2e..7e99f7ee 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -10,7 +10,7 @@ require ( github.com/tech/sendico/fx/oracle v0.0.0 github.com/tech/sendico/pkg v0.1.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index e9ad2722..35825f0b 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -152,16 +152,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/discovery/go.mod b/api/discovery/go.mod index bcda6b99..c1005c0b 100644 --- a/api/discovery/go.mod +++ b/api/discovery/go.mod @@ -44,6 +44,6 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc v1.79.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/discovery/go.sum b/api/discovery/go.sum index e9ad2722..35825f0b 100644 --- a/api/discovery/go.sum +++ b/api/discovery/go.sum @@ -152,16 +152,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index e6db5199..a9defa7e 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -48,6 +48,6 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc v1.79.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index e9ad2722..35825f0b 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -152,16 +152,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/ingestor/internal/ingestor/service_test.go b/api/fx/ingestor/internal/ingestor/service_test.go index 16f9b2e1..7087a467 100644 --- a/api/fx/ingestor/internal/ingestor/service_test.go +++ b/api/fx/ingestor/internal/ingestor/service_test.go @@ -200,7 +200,7 @@ type repositoryStub struct { 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) Quotes() quotestorage.QuotesStore { return nil } func (r *repositoryStub) Pairs() storage.PairStore { return nil } func (r *repositoryStub) Currencies() storage.CurrencyStore { return nil } diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index d8ab34bc..b7ea0e59 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -13,7 +13,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index e9ad2722..35825f0b 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -152,16 +152,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/oracle/internal/service/oracle/service_test.go b/api/fx/oracle/internal/service/oracle/service_test.go index be573e63..7857f608 100644 --- a/api/fx/oracle/internal/service/oracle/service_test.go +++ b/api/fx/oracle/internal/service/oracle/service_test.go @@ -19,16 +19,16 @@ import ( type repositoryStub struct { rates storage.RatesStore - quotes storage.QuotesStore + quotes quotestorage.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) Ping(ctx context.Context) error { return r.pingErr } +func (r *repositoryStub) Rates() storage.RatesStore { return r.rates } +func (r *repositoryStub) Quotes() quotestorage.QuotesStore { return r.quotes } +func (r *repositoryStub) Pairs() storage.PairStore { return r.pairs } func (r *repositoryStub) Currencies() storage.CurrencyStore { return r.currencies } diff --git a/api/fx/storage/mongo/repository.go b/api/fx/storage/mongo/repository.go index fd469fde..5d02793f 100644 --- a/api/fx/storage/mongo/repository.go +++ b/api/fx/storage/mongo/repository.go @@ -21,7 +21,7 @@ type Store struct { txFactory transaction.Factory rates storage.RatesStore - quotes storage.QuotesStore + quotes quotestorage.QuotesStore pairs storage.PairStore currencies storage.CurrencyStore } @@ -92,7 +92,7 @@ func (s *Store) Rates() storage.RatesStore { return s.rates } -func (s *Store) Quotes() storage.QuotesStore { +func (s *Store) Quotes() quotestorage.QuotesStore { return s.quotes } diff --git a/api/fx/storage/mongo/store/quotes.go b/api/fx/storage/mongo/store/quotes.go index 22201768..a97026f4 100644 --- a/api/fx/storage/mongo/store/quotes.go +++ b/api/fx/storage/mongo/store/quotes.go @@ -23,7 +23,7 @@ type quotesStore struct { txFactory transaction.Factory } -func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.Factory) (storage.QuotesStore, error) { +func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.Factory) (quotestorage.QuotesStore, error) { repo := repository.CreateMongoRepository(db, model.QuotesCollection) indexes := []*ri.Definition{ { diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod index 70ac074a..bdf94ce7 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -15,7 +15,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index ac0b2ebb..0adbce5a 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -298,16 +298,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -362,8 +362,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod index e09985e9..6bdedf08 100644 --- a/api/gateway/mntx/go.mod +++ b/api/gateway/mntx/go.mod @@ -11,7 +11,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum index 0d812dcc..546641fd 100644 --- a/api/gateway/mntx/go.sum +++ b/api/gateway/mntx/go.sum @@ -154,16 +154,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -212,8 +212,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/gateway/tgsettle/go.mod b/api/gateway/tgsettle/go.mod index 0f738c0d..991c6fff 100644 --- a/api/gateway/tgsettle/go.mod +++ b/api/gateway/tgsettle/go.mod @@ -8,7 +8,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/gateway/tgsettle/go.sum b/api/gateway/tgsettle/go.sum index e9ad2722..35825f0b 100644 --- a/api/gateway/tgsettle/go.sum +++ b/api/gateway/tgsettle/go.sum @@ -152,16 +152,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/gateway/tron/go.mod b/api/gateway/tron/go.mod index f78dcb1b..aaa406d1 100644 --- a/api/gateway/tron/go.mod +++ b/api/gateway/tron/go.mod @@ -17,7 +17,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/gateway/tron/go.sum b/api/gateway/tron/go.sum index 4e88971e..21f0b502 100644 --- a/api/gateway/tron/go.sum +++ b/api/gateway/tron/go.sum @@ -313,16 +313,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -383,8 +383,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index 41a81fc6..4c30bd16 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -11,7 +11,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 88b3b986..80161577 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -154,16 +154,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -212,8 +212,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/notification/go.mod b/api/notification/go.mod index ea61d3ca..ef296a3f 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -51,6 +51,6 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.78.0 // indirect + google.golang.org/grpc v1.79.0 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/notification/go.sum b/api/notification/go.sum index fb432038..8246be3b 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -167,16 +167,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -227,8 +227,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/methods/.air.toml b/api/payments/methods/.air.toml new file mode 100644 index 00000000..16f8c34b --- /dev/null +++ b/api/payments/methods/.air.toml @@ -0,0 +1,46 @@ +root = "." +testdata_dir = "testdata" +tmp_dir = "tmp" + +[build] + args_bin = [] + entrypoint = "./tmp/main" + cmd = "go build -o ./tmp/main ." + delay = 1000 + exclude_dir = ["assets", "tmp", "vendor", "testdata"] + exclude_file = [] + exclude_regex = ["_test.go", "_templ.go"] + exclude_unchanged = false + follow_symlink = false + full_bin = "" + include_dir = [] + include_ext = ["go", "tpl", "tmpl", "html"] + include_file = [] + kill_delay = "0s" + log = "build-errors.log" + poll = false + poll_interval = 0 + post_cmd = [] + pre_cmd = [] + rerun = false + rerun_delay = 500 + send_interrupt = false + stop_on_error = false + +[color] + app = "" + build = "yellow" + main = "magenta" + runner = "green" + watcher = "cyan" + +[log] + main_only = false + time = false + +[misc] + clean_on_exit = false + +[screen] + clear_on_rebuild = false + keep_scroll = true diff --git a/api/payments/methods/client/client.go b/api/payments/methods/client/client.go new file mode 100644 index 00000000..b87d0564 --- /dev/null +++ b/api/payments/methods/client/client.go @@ -0,0 +1,131 @@ +package client + +import ( + "context" + "crypto/tls" + "fmt" + "strings" + "time" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" + "google.golang.org/grpc" + "google.golang.org/grpc/credentials" + "google.golang.org/grpc/credentials/insecure" +) + +// Client exposes typed helpers around the payment methods gRPC API. +type Client interface { + CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) + GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error) + UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) + DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error) + SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error) + ListPaymentMethods(ctx context.Context, req *methodsv1.ListPaymentMethodsRequest) (*methodsv1.ListPaymentMethodsResponse, error) + Close() error +} + +type grpcPaymentMethodsClient interface { + CreatePaymentMethod(ctx context.Context, in *methodsv1.CreatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.CreatePaymentMethodResponse, error) + GetPaymentMethod(ctx context.Context, in *methodsv1.GetPaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.GetPaymentMethodResponse, error) + UpdatePaymentMethod(ctx context.Context, in *methodsv1.UpdatePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.UpdatePaymentMethodResponse, error) + DeletePaymentMethod(ctx context.Context, in *methodsv1.DeletePaymentMethodRequest, opts ...grpc.CallOption) (*methodsv1.DeletePaymentMethodResponse, error) + SetPaymentMethodArchived(ctx context.Context, in *methodsv1.SetPaymentMethodArchivedRequest, opts ...grpc.CallOption) (*methodsv1.SetPaymentMethodArchivedResponse, error) + ListPaymentMethods(ctx context.Context, in *methodsv1.ListPaymentMethodsRequest, opts ...grpc.CallOption) (*methodsv1.ListPaymentMethodsResponse, error) +} + +type paymentMethodsClient struct { + cfg Config + conn *grpc.ClientConn + client grpcPaymentMethodsClient +} + +// New dials the payment methods 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("payment-methods: 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.InternalWrap(err, fmt.Sprintf("payment-methods: dial %s", cfg.Address)) + } + + return &paymentMethodsClient{ + cfg: cfg, + conn: conn, + client: methodsv1.NewPaymentMethodsServiceClient(conn), + }, nil +} + +// NewWithClient injects a pre-built payment methods client (useful for tests). +func NewWithClient(cfg Config, c grpcPaymentMethodsClient) Client { + cfg.setDefaults() + return &paymentMethodsClient{ + cfg: cfg, + client: c, + } +} + +func (c *paymentMethodsClient) Close() error { + if c.conn != nil { + return c.conn.Close() + } + return nil +} + +func (c *paymentMethodsClient) callContext(ctx context.Context) (context.Context, context.CancelFunc) { + timeout := c.cfg.CallTimeout + if timeout <= 0 { + timeout = 3 * time.Second + } + return context.WithTimeout(ctx, timeout) +} + +func (c *paymentMethodsClient) CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + return c.client.CreatePaymentMethod(callCtx, req) +} + +func (c *paymentMethodsClient) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + return c.client.GetPaymentMethod(callCtx, req) +} + +func (c *paymentMethodsClient) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + return c.client.UpdatePaymentMethod(callCtx, req) +} + +func (c *paymentMethodsClient) DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + return c.client.DeletePaymentMethod(callCtx, req) +} + +func (c *paymentMethodsClient) SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + return c.client.SetPaymentMethodArchived(callCtx, req) +} + +func (c *paymentMethodsClient) ListPaymentMethods(ctx context.Context, req *methodsv1.ListPaymentMethodsRequest) (*methodsv1.ListPaymentMethodsResponse, error) { + callCtx, cancel := c.callContext(ctx) + defer cancel() + return c.client.ListPaymentMethods(callCtx, req) +} diff --git a/api/payments/methods/client/config.go b/api/payments/methods/client/config.go new file mode 100644 index 00000000..272bd4ab --- /dev/null +++ b/api/payments/methods/client/config.go @@ -0,0 +1,20 @@ +package client + +import "time" + +// Config captures connection settings for the payment methods 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/methods/config.dev.yml b/api/payments/methods/config.dev.yml new file mode 100644 index 00000000..ee32ba01 --- /dev/null +++ b/api/payments/methods/config.dev.yml @@ -0,0 +1,49 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50066" + advertise_host: "dev-payments-methods" + enable_reflection: true + enable_health: true + +metrics: + address: ":9416" + +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: Payments Methods Service + max_reconnects: 10 + reconnect_wait: 5 + buffer_size: 1024 + +database: + driver: mongodb + settings: + host_env: PAYMENTS_MONGO_HOST + port_env: PAYMENTS_MONGO_PORT + database_env: PAYMENTS_MONGO_DATABASE + user_env: PAYMENTS_MONGO_USER + password_env: PAYMENTS_MONGO_PASSWORD + auth_source_env: PAYMENTS_MONGO_AUTH_SOURCE + replica_set_env: PAYMENTS_MONGO_REPLICA_SET + +permissions_database: + driver: mongodb + settings: + host_env: MONGO_HOST + port_env: MONGO_PORT + database_env: MONGO_DATABASE + user_env: MONGO_USER + password_env: MONGO_PASSWORD + auth_source_env: MONGO_AUTH_SOURCE + replica_set_env: MONGO_REPLICA_SET + enforcer: + driver: native diff --git a/api/payments/methods/config.yml b/api/payments/methods/config.yml new file mode 100644 index 00000000..966a7890 --- /dev/null +++ b/api/payments/methods/config.yml @@ -0,0 +1,49 @@ +runtime: + shutdown_timeout_seconds: 15 + +grpc: + network: tcp + address: ":50066" + advertise_host: "sendico_payments_methods" + enable_reflection: true + enable_health: true + +metrics: + address: ":9416" + +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: Payments Methods Service + max_reconnects: 10 + reconnect_wait: 5 + buffer_size: 1024 + +database: + driver: mongodb + settings: + host_env: PAYMENTS_MONGO_HOST + port_env: PAYMENTS_MONGO_PORT + database_env: PAYMENTS_MONGO_DATABASE + user_env: PAYMENTS_MONGO_USER + password_env: PAYMENTS_MONGO_PASSWORD + auth_source_env: PAYMENTS_MONGO_AUTH_SOURCE + replica_set_env: PAYMENTS_MONGO_REPLICA_SET + +permissions_database: + driver: mongodb + settings: + host_env: MONGO_HOST + port_env: MONGO_PORT + database_env: MONGO_DATABASE + user_env: MONGO_USER + password_env: MONGO_PASSWORD + auth_source_env: MONGO_AUTH_SOURCE + replica_set_env: MONGO_REPLICA_SET + enforcer: + driver: native diff --git a/api/payments/methods/env/.gitignore b/api/payments/methods/env/.gitignore new file mode 100644 index 00000000..f2a8cbe3 --- /dev/null +++ b/api/payments/methods/env/.gitignore @@ -0,0 +1 @@ +.env.api diff --git a/api/payments/methods/go.mod b/api/payments/methods/go.mod new file mode 100644 index 00000000..75040980 --- /dev/null +++ b/api/payments/methods/go.mod @@ -0,0 +1,52 @@ +module github.com/tech/sendico/payments/methods + +go 1.25.7 + +replace github.com/tech/sendico/pkg => ../../pkg + +replace github.com/tech/sendico/payments/storage => ../storage + +require ( + github.com/tech/sendico/payments/storage v0.0.0-00010101000000-000000000000 + github.com/tech/sendico/pkg v0.1.0 + go.mongodb.org/mongo-driver/v2 v2.5.0 + go.uber.org/zap v1.27.1 + google.golang.org/grpc v1.79.0 + gopkg.in/yaml.v3 v3.0.1 +) + +require ( + github.com/beorn7/perks v1.0.1 // indirect + github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect + github.com/casbin/casbin/v2 v2.135.0 // indirect + github.com/casbin/govaluate v1.10.0 // indirect + github.com/casbin/mongodb-adapter/v4 v4.3.0 // indirect + github.com/cespare/xxhash/v2 v2.3.0 // indirect + github.com/go-chi/chi/v5 v5.2.5 // indirect + github.com/google/uuid v1.6.0 // indirect + github.com/klauspost/compress v1.18.4 // 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/munnerz/goautoneg v0.0.0-20191010083416-a7dc8b61c822 // indirect + github.com/nats-io/nats.go v1.48.0 // indirect + github.com/nats-io/nkeys v0.4.15 // indirect + github.com/nats-io/nuid v1.0.1 // indirect + github.com/prometheus/client_golang v1.23.2 // indirect + github.com/prometheus/client_model v0.6.2 // indirect + github.com/prometheus/common v0.67.5 // indirect + github.com/prometheus/procfs v0.19.2 // indirect + github.com/xdg-go/pbkdf2 v1.0.0 // indirect + github.com/xdg-go/scram v1.2.0 // 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.48.0 // indirect + golang.org/x/net v0.50.0 // indirect + golang.org/x/sync v0.19.0 // indirect + golang.org/x/sys v0.41.0 // indirect + golang.org/x/text v0.34.0 // indirect + google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect + google.golang.org/protobuf v1.36.11 // indirect +) diff --git a/api/payments/methods/go.sum b/api/payments/methods/go.sum new file mode 100644 index 00000000..35825f0b --- /dev/null +++ b/api/payments/methods/go.sum @@ -0,0 +1,221 @@ +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.10.0 h1:zU9WiOla1YA122oLM6i4EXvGW62DvKZVxIe6TYWexEs= +github.com/bmatcuk/doublestar/v4 v4.10.0/go.mod h1:xBQ8jztBU6kakFMg+8WGxn0c6z1fTSPVIjEY1Wr7jzc= +github.com/casbin/casbin/v2 v2.135.0 h1:6BLkMQiGotYyS5yYeWgW19vxqugUlvHFkFiLnLR/bxk= +github.com/casbin/casbin/v2 v2.135.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/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0lCi7JKrS4qQ= +github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE= +github.com/cenkalti/backoff/v4 v4.3.0 h1:MyRJ/UdXutAwSAT+s3wNd7MfTIcy71VQueUuFK343L8= +github.com/cenkalti/backoff/v4 v4.3.0/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.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug= +github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0= +github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= +github.com/go-logr/logr v1.4.3/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/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.4 h1:RPhnKRAQ4Fh8zU2FY/6ZFDwTVTxgJ/EMydqSTzE9a2c= +github.com/klauspost/compress v1.18.4/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4= +github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE= +github.com/kr/pretty v0.3.1/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/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.48.0 h1:pSFyXApG+yWU/TgbKCjmm5K4wrHu86231/w84qRVR+U= +github.com/nats-io/nats.go v1.48.0/go.mod h1:iRWIPokVIFbVijxuMQq4y9ttaBTMe0SFdlZfMDd+33g= +github.com/nats-io/nkeys v0.4.15 h1:JACV5jRVO9V856KOapQ7x+EY8Jo3qw1vJt/9Jpwzkk4= +github.com/nats-io/nkeys v0.4.15/go.mod h1:CpMchTXC9fxA5zrMo4KpySxNjiDVvr8ANOSZdiNfUrs= +github.com/nats-io/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.5 h1:pIgK94WWlQt1WLwAC5j2ynLaBRDiinoAb86HZHTUGI4= +github.com/prometheus/common v0.67.5/go.mod h1:SjE/0MzDEEAyrdr5Gqc6G+sXI67maCxzaT3A2+HqjUw= +github.com/prometheus/procfs v0.19.2 h1:zUMhqEW66Ex7OXIiDkll3tl9a1ZdilUOd/F6ZXw4Vws= +github.com/prometheus/procfs v0.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.2.0 h1:bYKF2AEwG5rqd1BumT4gAnvwU/M9nBp2pTSxeZw7Wvs= +github.com/xdg-go/scram v1.2.0/go.mod h1:3dlrS0iBaWKYVt2ZfA4cj48umJZ+cAEbR6/SjLA88I8= +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/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE= +go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0= +go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64= +go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 h1:UP6IpuHFkUgOQL9FFQFrZ+5LiwhhYRbi7VZSIx6Nj5s= +go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0/go.mod h1:qxuZLtbq5QDtdeSHsS7bcf6EH6uO6jUAgk764zd3rhM= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= +go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= +go.uber.org/goleak v1.3.0/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.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc= +go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E= +go.yaml.in/yaml/v2 v2.4.3 h1:6gvOSjQoTB3vt1l+CU+tSyi/HOjfOjRLJ4YwYZGwRO0= +go.yaml.in/yaml/v2 v2.4.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.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts= +golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos= +golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= +golang.org/x/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.50.0 h1:ucWh9eiCGyDR3vtzso0WMQinm2Dnt8cFMuQa9K33J60= +golang.org/x/net v0.50.0/go.mod h1:UgoSli3F/pBgdJBHCTc+tp3gmrU4XswgGRgtnwWTfyM= +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.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.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.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k= +golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= +golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= +golang.org/x/term v0.0.0-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.34.0 h1:oL/Qq0Kdaqxa1KbNeMKwQq0reLCCaFtqu2eNuSeNHbk= +golang.org/x/text v0.34.0/go.mod h1:homfLqTYRFyVYemLBFl5GgL/DWEiH5wcsQ5gSh1yziA= +golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= +golang.org/x/tools v0.0.0-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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= +google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= +google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= +gopkg.in/check.v1 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/methods/internal/appversion/version.go b/api/payments/methods/internal/appversion/version.go new file mode 100644 index 00000000..88e7ee1e --- /dev/null +++ b/api/payments/methods/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 via ldflags. +var ( + Version string + Revision string + Branch string + BuildUser string + BuildDate string +) + +// Create returns a printer configured for the payment methods service. +func Create() version.Printer { + vi := version.Info{ + Program: "Sendico Payment Methods Service", + Revision: Revision, + Branch: Branch, + BuildUser: BuildUser, + BuildDate: BuildDate, + Version: Version, + } + return vf.Create(&vi) +} diff --git a/api/payments/methods/internal/server/internal/config.go b/api/payments/methods/internal/server/internal/config.go new file mode 100644 index 00000000..40b42677 --- /dev/null +++ b/api/payments/methods/internal/server/internal/config.go @@ -0,0 +1,62 @@ +package serverimp + +import ( + "os" + "strings" + + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" + "gopkg.in/yaml.v3" +) + +type config struct { + *grpcapp.Config `yaml:",inline"` + + // PermissionsDatabase points to the authorization store (policies/roles/assignments). + // If omitted, startup falls back to Database for backward compatibility. + PermissionsDatabase *db.Config `yaml:"permissions_database"` +} + +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: ":50066", + EnableReflection: true, + EnableHealth: true, + } + } else { + if strings.TrimSpace(cfg.GRPC.Address) == "" { + cfg.GRPC.Address = ":50066" + } + if strings.TrimSpace(cfg.GRPC.Network) == "" { + cfg.GRPC.Network = "tcp" + } + } + + if cfg.Metrics == nil { + cfg.Metrics = &grpcapp.MetricsConfig{Address: ":9416"} + } else if strings.TrimSpace(cfg.Metrics.Address) == "" { + cfg.Metrics.Address = ":9416" + } + + return cfg, nil +} diff --git a/api/payments/methods/internal/server/internal/discovery.go b/api/payments/methods/internal/server/internal/discovery.go new file mode 100644 index 00000000..7a0cb157 --- /dev/null +++ b/api/payments/methods/internal/server/internal/discovery.go @@ -0,0 +1,88 @@ +package serverimp + +import ( + "strings" + + "github.com/tech/sendico/payments/methods/internal/appversion" + "github.com/tech/sendico/pkg/discovery" + msg "github.com/tech/sendico/pkg/messaging" + "go.uber.org/zap" +) + +const methodsDiscoverySender = "payment_methods" + +func (i *Imp) initDiscovery(cfg *config) { + if i == nil || cfg == nil || cfg.Messaging == nil || cfg.Messaging.Driver == "" { + return + } + + logger := i.logger.Named("discovery") + broker, err := msg.CreateMessagingBroker(logger.Named("bus"), cfg.Messaging) + if err != nil { + i.logger.Warn("Failed to initialise discovery broker", zap.Error(err)) + return + } + + registry := discovery.NewRegistry() + watcher, err := discovery.NewRegistryWatcher(logger, broker, registry) + if err != nil { + i.logger.Warn("Failed to initialise discovery registry watcher", zap.Error(err)) + return + } + if err := watcher.Start(); err != nil { + i.logger.Warn("Failed to start discovery registry watcher", zap.Error(err)) + return + } + + i.discoveryWatcher = watcher + i.discoveryReg = registry + i.logger.Info("Discovery registry watcher started") +} + +func (i *Imp) startDiscoveryAnnouncer(cfg *config, producer msg.Producer) { + if i == nil || cfg == nil || producer == nil || cfg.GRPC == nil { + return + } + + invokeURI := strings.TrimSpace(cfg.GRPC.DiscoveryInvokeURI()) + if invokeURI == "" { + i.logger.Warn("Skipping discovery announcement: missing advertise host/port in gRPC config") + return + } + + announce := discovery.Announcement{ + Service: "PAYMENTS_METHODS", + Operations: []string{ + "payment_methods.manage", + "payment_methods.read", + }, + InvokeURI: invokeURI, + Version: appversion.Create().Short(), + } + + i.discoveryAnnouncer = discovery.NewAnnouncer(i.logger, producer, methodsDiscoverySender, announce) + i.discoveryAnnouncer.Start() + i.logger.Info("Discovery announcer started", + zap.String("service", announce.Service), + zap.String("invoke_uri", announce.InvokeURI)) +} + +func (i *Imp) stopDiscoveryAnnouncer() { + if i == nil || i.discoveryAnnouncer == nil { + return + } + i.discoveryAnnouncer.Stop() + i.discoveryAnnouncer = nil +} + +func (i *Imp) stopDiscovery() { + if i == nil { + return + } + i.stopDiscoveryAnnouncer() + if i.discoveryWatcher != nil { + i.discoveryWatcher.Stop() + i.discoveryWatcher = nil + } + i.discoveryReg = nil +} diff --git a/api/payments/methods/internal/server/internal/lifecycle.go b/api/payments/methods/internal/server/internal/lifecycle.go new file mode 100644 index 00000000..b28f155f --- /dev/null +++ b/api/payments/methods/internal/server/internal/lifecycle.go @@ -0,0 +1,16 @@ +package serverimp + +import "context" + +func (i *Imp) shutdownApp() { + if i == nil || i.app == nil { + return + } + + timeout := i.config.Runtime.ShutdownTimeout() + ctx, cancel := context.WithTimeout(context.Background(), timeout) + defer cancel() + + i.app.Shutdown(ctx) + i.app = nil +} diff --git a/api/payments/methods/internal/server/internal/serverimp.go b/api/payments/methods/internal/server/internal/serverimp.go new file mode 100644 index 00000000..e52563c3 --- /dev/null +++ b/api/payments/methods/internal/server/internal/serverimp.go @@ -0,0 +1,117 @@ +package serverimp + +import ( + "context" + + "github.com/tech/sendico/payments/methods/internal/service/methods" + "github.com/tech/sendico/payments/storage" + mongostorage "github.com/tech/sendico/payments/storage/mongo" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/merrors" + msg "github.com/tech/sendico/pkg/messaging" + mb "github.com/tech/sendico/pkg/messaging/broker" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/mservice" + "github.com/tech/sendico/pkg/server/grpcapp" + "go.uber.org/zap" +) + +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() { + i.stopDiscovery() + if i.service != nil { + i.service.Shutdown() + } + i.shutdownApp() + if i.dbFactory != nil { + i.dbFactory.CloseConnection() + i.dbFactory = nil + } +} + +func (i *Imp) Start() error { + cfg, err := i.loadConfig() + if err != nil { + return err + } + i.config = cfg + + i.initDiscovery(cfg) + + if cfg.Database == nil { + return merrors.InvalidArgument("database configuration is required") + } + + permissionsDB := cfg.PermissionsDatabase + if permissionsDB == nil { + i.logger.Info("permissions_database is not configured, falling back to database settings") + permissionsDB = cfg.Database + } + + i.dbFactory, err = db.NewConnection(i.logger, permissionsDB) + if err != nil { + return err + } + + policy, err := i.dbFactory.Permissions().GetPolicyDescription(context.Background(), mservice.PaymentMethods) + if err != nil { + i.dbFactory.CloseConnection() + i.dbFactory = nil + return err + } + + var broker mb.Broker + if cfg.Messaging != nil && cfg.Messaging.Driver != "" { + broker, err = msg.CreateMessagingBroker(i.logger, cfg.Messaging) + if err != nil { + i.logger.Warn("Failed to create recipient notifications broker", zap.Error(err)) + } + } + + repoFactory := func(logger mlogger.Logger, conn *db.MongoConnection) (storage.Repository, error) { + return mongostorage.New( + logger, + conn, + mongostorage.WithPaymentMethodsAuth(i.dbFactory.Permissions().Enforcer(), policy.ID), + ) + } + + serviceFactory := func(logger mlogger.Logger, repo storage.Repository, producer msg.Producer) (grpcapp.Service, error) { + opts := []methods.Option{} + if broker != nil { + opts = append(opts, methods.WithRecipientEventsBroker(broker)) + } + + i.startDiscoveryAnnouncer(cfg, producer) + svc, err := methods.NewService(logger, repo, opts...) + if err != nil { + return nil, err + } + i.service = svc + return svc, nil + } + + app, err := grpcapp.NewApp(i.logger, "payments_methods", cfg.Config, i.debug, repoFactory, serviceFactory) + if err != nil { + i.dbFactory.CloseConnection() + i.dbFactory = nil + return err + } + i.app = app + + if err := i.app.Start(); err != nil { + if i.dbFactory != nil { + i.dbFactory.CloseConnection() + i.dbFactory = nil + } + return err + } + return nil +} diff --git a/api/payments/methods/internal/server/internal/types.go b/api/payments/methods/internal/server/internal/types.go new file mode 100644 index 00000000..d68b2351 --- /dev/null +++ b/api/payments/methods/internal/server/internal/types.go @@ -0,0 +1,29 @@ +package serverimp + +import ( + "github.com/tech/sendico/payments/storage" + "github.com/tech/sendico/pkg/db" + "github.com/tech/sendico/pkg/discovery" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server/grpcapp" +) + +type methodsService interface { + grpcapp.Service + Shutdown() +} + +type Imp struct { + logger mlogger.Logger + file string + debug bool + + config *config + app *grpcapp.App[storage.Repository] + service methodsService + dbFactory db.Factory + + discoveryWatcher *discovery.RegistryWatcher + discoveryReg *discovery.Registry + discoveryAnnouncer *discovery.Announcer +} diff --git a/api/payments/methods/internal/server/server.go b/api/payments/methods/internal/server/server.go new file mode 100644 index 00000000..5b926c60 --- /dev/null +++ b/api/payments/methods/internal/server/server.go @@ -0,0 +1,12 @@ +package server + +import ( + serverimp "github.com/tech/sendico/payments/methods/internal/server/internal" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/server" +) + +// Create initialises the payment methods server implementation. +func Create(logger mlogger.Logger, file string, debug bool) (server.Application, error) { + return serverimp.Create(logger, file, debug) +} diff --git a/api/payments/methods/internal/service/methods/archive.go b/api/payments/methods/internal/service/methods/archive.go new file mode 100644 index 00000000..536709e7 --- /dev/null +++ b/api/payments/methods/internal/service/methods/archive.go @@ -0,0 +1,36 @@ +package methods + +import ( + "context" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" +) + +func (s *Service) SetPaymentMethodArchived(ctx context.Context, req *methodsv1.SetPaymentMethodArchivedRequest) (*methodsv1.SetPaymentMethodArchivedResponse, error) { + if req == nil { + return autoError[methodsv1.SetPaymentMethodArchivedResponse](ctx, s.logger, merrors.InvalidArgument("request is required")) + } + if s.pmstore == nil { + return autoError[methodsv1.SetPaymentMethodArchivedResponse](ctx, s.logger, errStoreUnavailable) + } + + accountRef, err := parseObjectID(req.GetAccountRef(), "account_ref") + if err != nil { + return autoError[methodsv1.SetPaymentMethodArchivedResponse](ctx, s.logger, err) + } + organizationRef, err := parseObjectID(req.GetOrganizationRef(), "organization_ref") + if err != nil { + return autoError[methodsv1.SetPaymentMethodArchivedResponse](ctx, s.logger, err) + } + methodRef, err := parseObjectID(req.GetPaymentMethodRef(), "payment_method_ref") + if err != nil { + return autoError[methodsv1.SetPaymentMethodArchivedResponse](ctx, s.logger, err) + } + + if err := s.pmstore.SetArchived(ctx, accountRef, organizationRef, methodRef, req.GetArchived(), req.GetCascade()); err != nil { + return autoError[methodsv1.SetPaymentMethodArchivedResponse](ctx, s.logger, err) + } + + return &methodsv1.SetPaymentMethodArchivedResponse{}, nil +} diff --git a/api/payments/methods/internal/service/methods/create.go b/api/payments/methods/internal/service/methods/create.go new file mode 100644 index 00000000..4f5eb58c --- /dev/null +++ b/api/payments/methods/internal/service/methods/create.go @@ -0,0 +1,41 @@ +package methods + +import ( + "context" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" +) + +func (s *Service) CreatePaymentMethod(ctx context.Context, req *methodsv1.CreatePaymentMethodRequest) (*methodsv1.CreatePaymentMethodResponse, error) { + if req == nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, merrors.InvalidArgument("request is required")) + } + if s.pmstore == nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, errStoreUnavailable) + } + + accountRef, err := parseObjectID(req.GetAccountRef(), "account_ref") + if err != nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) + } + organizationRef, err := parseObjectID(req.GetOrganizationRef(), "organization_ref") + if err != nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) + } + + pm, err := decodePaymentMethod(req.GetPaymentMethodJson()) + if err != nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) + } + if err := s.pmstore.Create(ctx, accountRef, organizationRef, pm); err != nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) + } + + payload, err := encodePaymentMethod(pm) + if err != nil { + return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) + } + + return &methodsv1.CreatePaymentMethodResponse{PaymentMethodJson: payload}, nil +} diff --git a/api/payments/methods/internal/service/methods/delete.go b/api/payments/methods/internal/service/methods/delete.go new file mode 100644 index 00000000..4e3aba35 --- /dev/null +++ b/api/payments/methods/internal/service/methods/delete.go @@ -0,0 +1,37 @@ +package methods + +import ( + "context" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" +) + +func (s *Service) DeletePaymentMethod(ctx context.Context, req *methodsv1.DeletePaymentMethodRequest) (*methodsv1.DeletePaymentMethodResponse, error) { + if req == nil { + return autoError[methodsv1.DeletePaymentMethodResponse](ctx, s.logger, merrors.InvalidArgument("request is required")) + } + if s.pmstore == nil { + return autoError[methodsv1.DeletePaymentMethodResponse](ctx, s.logger, errStoreUnavailable) + } + + accountRef, err := parseObjectID(req.GetAccountRef(), "account_ref") + if err != nil { + return autoError[methodsv1.DeletePaymentMethodResponse](ctx, s.logger, err) + } + methodRef, err := parseObjectID(req.GetPaymentMethodRef(), "payment_method_ref") + if err != nil { + return autoError[methodsv1.DeletePaymentMethodResponse](ctx, s.logger, err) + } + + if req.GetCascade() { + err = s.pmstore.DeleteCascade(ctx, accountRef, methodRef) + } else { + err = s.pmstore.Delete(ctx, accountRef, methodRef) + } + if err != nil { + return autoError[methodsv1.DeletePaymentMethodResponse](ctx, s.logger, err) + } + + return &methodsv1.DeletePaymentMethodResponse{}, nil +} diff --git a/api/payments/methods/internal/service/methods/get.go b/api/payments/methods/internal/service/methods/get.go new file mode 100644 index 00000000..d014767a --- /dev/null +++ b/api/payments/methods/internal/service/methods/get.go @@ -0,0 +1,38 @@ +package methods + +import ( + "context" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" +) + +func (s *Service) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymentMethodRequest) (*methodsv1.GetPaymentMethodResponse, error) { + if req == nil { + return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, merrors.InvalidArgument("request is required")) + } + if s.pmstore == nil { + return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, errStoreUnavailable) + } + + accountRef, err := parseObjectID(req.GetAccountRef(), "account_ref") + if err != nil { + return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err) + } + methodRef, err := parseObjectID(req.GetPaymentMethodRef(), "payment_method_ref") + if err != nil { + return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err) + } + + pm, err := s.pmstore.Get(ctx, accountRef, methodRef) + if err != nil { + return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err) + } + + payload, err := encodePaymentMethod(pm) + if err != nil { + return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err) + } + + return &methodsv1.GetPaymentMethodResponse{PaymentMethodJson: payload}, nil +} diff --git a/api/payments/methods/internal/service/methods/list.go b/api/payments/methods/internal/service/methods/list.go new file mode 100644 index 00000000..698406ce --- /dev/null +++ b/api/payments/methods/internal/service/methods/list.go @@ -0,0 +1,48 @@ +package methods + +import ( + "context" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" +) + +func (s *Service) ListPaymentMethods(ctx context.Context, req *methodsv1.ListPaymentMethodsRequest) (*methodsv1.ListPaymentMethodsResponse, error) { + if req == nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, merrors.InvalidArgument("request is required")) + } + if s.pmstore == nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, errStoreUnavailable) + } + + accountRef, err := parseObjectID(req.GetAccountRef(), "account_ref") + if err != nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) + } + organizationRef, err := parseObjectID(req.GetOrganizationRef(), "organization_ref") + if err != nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) + } + recipientRef, err := parseObjectID(req.GetRecipientRef(), "recipient_ref") + if err != nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) + } + + items, err := s.pmstore.List(ctx, accountRef, organizationRef, recipientRef, toModelCursor(req.GetCursor())) + if err != nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) + } + + result := make([][]byte, 0, len(items)) + for i := range items { + payload, err := encodePaymentMethod(&items[i]) + if err != nil { + return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) + } + result = append(result, payload) + } + + return &methodsv1.ListPaymentMethodsResponse{ + PaymentMethodsJson: result, + }, nil +} diff --git a/api/payments/methods/internal/service/methods/recipient_consumer.go b/api/payments/methods/internal/service/methods/recipient_consumer.go new file mode 100644 index 00000000..19800e94 --- /dev/null +++ b/api/payments/methods/internal/service/methods/recipient_consumer.go @@ -0,0 +1,87 @@ +package methods + +import ( + "context" + + cons "github.com/tech/sendico/pkg/messaging/consumer" + objectnotifications "github.com/tech/sendico/pkg/messaging/notifications/object" + np "github.com/tech/sendico/pkg/messaging/notifications/processor" + nm "github.com/tech/sendico/pkg/model/notification" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/v2/bson" + "go.uber.org/zap" +) + +func (s *Service) startRecipientConsumers() { + if s == nil || s.recipientBroker == nil { + s.logger.Warn("Missing broker. Recipient cascade consumers have NOT started") + return + } + + s.consumeRecipientProcessor( + objectnotifications.NewObjectChangedMessageProcessor(s.logger, mservice.Recipients, nm.NAArchived, s.onRecipientNotification), + ) + s.consumeRecipientProcessor( + objectnotifications.NewObjectChangedMessageProcessor(s.logger, mservice.Recipients, nm.NADeleted, s.onRecipientNotification), + ) + + s.logger.Info("Recipient cascade consumers started") +} + +func (s *Service) consumeRecipientProcessor(processor np.EnvelopeProcessor) { + consumer, err := cons.NewConsumer(s.logger, s.recipientBroker, processor.GetSubject()) + if err != nil { + s.logger.Warn("Failed to create recipient consumer", zap.Error(err), zap.String("event", processor.GetSubject().ToString())) + return + } + s.recipientConsumers = append(s.recipientConsumers, consumer) + + go func() { + if err := consumer.ConsumeMessages(processor.Process); err != nil { + s.logger.Warn("Recipient consumer stopped", zap.Error(err), zap.String("event", processor.GetSubject().ToString())) + } + }() +} + +func (s *Service) onRecipientNotification( + ctx context.Context, + objectType mservice.Type, + recipientRef, actorAccountRef bson.ObjectID, + action nm.NotificationAction, +) error { + if s.pmstore == nil { + return errStoreUnavailable + } + if objectType != mservice.Recipients || recipientRef == bson.NilObjectID { + return nil + } + + switch action { + case nm.NAArchived: + updated, err := s.pmstore.SetArchivedByRecipient(ctx, recipientRef, true) + if err != nil { + s.logger.Warn("Failed to cascade archive payment methods by recipient", + zap.Error(err), + zap.String("recipient_ref", recipientRef.Hex()), + zap.String("actor_account_ref", actorAccountRef.Hex())) + return err + } + s.logger.Info("Recipient archive cascade applied to payment methods", + zap.String("recipient_ref", recipientRef.Hex()), + zap.String("actor_account_ref", actorAccountRef.Hex()), + zap.Int("updated_count", updated)) + case nm.NADeleted: + if err := s.pmstore.DeleteByRecipient(ctx, recipientRef); err != nil { + s.logger.Warn("Failed to cascade delete payment methods by recipient", + zap.Error(err), + zap.String("recipient_ref", recipientRef.Hex()), + zap.String("actor_account_ref", actorAccountRef.Hex())) + return err + } + s.logger.Info("Recipient delete cascade applied to payment methods", + zap.String("recipient_ref", recipientRef.Hex()), + zap.String("actor_account_ref", actorAccountRef.Hex())) + } + + return nil +} diff --git a/api/payments/methods/internal/service/methods/service.go b/api/payments/methods/internal/service/methods/service.go new file mode 100644 index 00000000..0df8d498 --- /dev/null +++ b/api/payments/methods/internal/service/methods/service.go @@ -0,0 +1,90 @@ +package methods + +import ( + "github.com/tech/sendico/payments/storage" + "github.com/tech/sendico/pkg/api/routers" + "github.com/tech/sendico/pkg/merrors" + msg "github.com/tech/sendico/pkg/messaging" + mb "github.com/tech/sendico/pkg/messaging/broker" + "github.com/tech/sendico/pkg/mlogger" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" + "google.golang.org/grpc" +) + +var errStoreUnavailable = merrors.Internal("payment-methods: storage is not initialised") + +// Option configures service dependencies. +type Option func(*Service) + +// WithRecipientEventsBroker wires the broker used to consume recipient events. +func WithRecipientEventsBroker(broker mb.Broker) Option { + return func(s *Service) { + if broker != nil { + s.recipientBroker = broker + } + } +} + +// Service implements payments.methods.v1.PaymentMethodsService. +type Service struct { + logger mlogger.Logger + storage storage.Repository + pmstore storage.PaymentMethodsStore + + recipientBroker mb.Broker + recipientConsumers []msg.Consumer + + methodsv1.UnimplementedPaymentMethodsServiceServer +} + +// NewService creates a payment methods gRPC service. +func NewService(logger mlogger.Logger, repo storage.Repository, opts ...Option) (*Service, error) { + if logger == nil { + return nil, merrors.InvalidArgument("payment-methods: logger is required") + } + if repo == nil { + return nil, merrors.InvalidArgument("payment-methods: storage repository is required") + } + + pmstore := repo.PaymentMethods() + if pmstore == nil { + return nil, errStoreUnavailable + } + + svc := &Service{ + logger: logger.Named("payment_methods"), + storage: repo, + pmstore: pmstore, + } + + for _, opt := range opts { + if opt != nil { + opt(svc) + } + } + + svc.startRecipientConsumers() + return svc, nil +} + +// Register attaches the service to the supplied gRPC router. +func (s *Service) Register(router routers.GRPC) error { + return router.Register(func(reg grpc.ServiceRegistrar) { + methodsv1.RegisterPaymentMethodsServiceServer(reg, s) + }) +} + +// Shutdown releases underlying resources. +func (s *Service) Shutdown() { + if s == nil { + return + } + for _, consumer := range s.recipientConsumers { + if consumer != nil { + consumer.Close() + } + } + s.recipientConsumers = nil + s.pmstore = nil + s.storage = nil +} diff --git a/api/payments/methods/internal/service/methods/update.go b/api/payments/methods/internal/service/methods/update.go new file mode 100644 index 00000000..2f880c51 --- /dev/null +++ b/api/payments/methods/internal/service/methods/update.go @@ -0,0 +1,37 @@ +package methods + +import ( + "context" + + "github.com/tech/sendico/pkg/merrors" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" +) + +func (s *Service) UpdatePaymentMethod(ctx context.Context, req *methodsv1.UpdatePaymentMethodRequest) (*methodsv1.UpdatePaymentMethodResponse, error) { + if req == nil { + return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, merrors.InvalidArgument("request is required")) + } + if s.pmstore == nil { + return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, errStoreUnavailable) + } + + accountRef, err := parseObjectID(req.GetAccountRef(), "account_ref") + if err != nil { + return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) + } + + pm, err := decodePaymentMethod(req.GetPaymentMethodJson()) + if err != nil { + return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) + } + if err := s.pmstore.Update(ctx, accountRef, pm); err != nil { + return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) + } + + payload, err := encodePaymentMethod(pm) + if err != nil { + return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) + } + + return &methodsv1.UpdatePaymentMethodResponse{PaymentMethodJson: payload}, nil +} diff --git a/api/payments/methods/internal/service/methods/util.go b/api/payments/methods/internal/service/methods/util.go new file mode 100644 index 00000000..a2eaa539 --- /dev/null +++ b/api/payments/methods/internal/service/methods/util.go @@ -0,0 +1,83 @@ +package methods + +import ( + "context" + "encoding/json" + "fmt" + "strings" + + "github.com/tech/sendico/pkg/api/routers/gsresponse" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" + "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func autoError[T any](ctx context.Context, logger mlogger.Logger, err error) (*T, error) { + return gsresponse.Execute(ctx, gsresponse.Auto[T](logger, mservice.PaymentMethods, err)) +} + +func parseObjectID(value, field string) (bson.ObjectID, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s is required", field), field) + } + ref, err := bson.ObjectIDFromHex(trimmed) + if err != nil { + return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s must be a valid object id", field), field) + } + return ref, nil +} + +func decodePaymentMethod(data []byte) (*model.PaymentMethod, error) { + if len(data) == 0 { + return nil, merrors.InvalidArgument("payment_method_json is required", "payment_method_json") + } + res := &model.PaymentMethod{} + if err := json.Unmarshal(data, res); err != nil { + return nil, merrors.InvalidArgumentWrap(err, "failed to decode payment method", "payment_method_json") + } + return res, nil +} + +func encodePaymentMethod(pm *model.PaymentMethod) ([]byte, error) { + if pm == nil { + return nil, merrors.InvalidArgument("payment method is required") + } + payload, err := json.Marshal(pm) + if err != nil { + return nil, merrors.InternalWrap(err, "failed to encode payment method") + } + return payload, nil +} + +func toModelCursor(cursor *methodsv1.ViewCursor) *model.ViewCursor { + if cursor == nil { + return nil + } + + res := &model.ViewCursor{} + hasAny := false + + if limit := cursor.GetLimit(); limit != nil { + v := limit.GetValue() + res.Limit = &v + hasAny = true + } + if offset := cursor.GetOffset(); offset != nil { + v := offset.GetValue() + res.Offset = &v + hasAny = true + } + if archived := cursor.GetIsArchived(); archived != nil { + v := archived.GetValue() + res.IsArchived = &v + hasAny = true + } + if !hasAny { + return nil + } + return res +} diff --git a/api/payments/methods/main.go b/api/payments/methods/main.go new file mode 100644 index 00000000..8db5a301 --- /dev/null +++ b/api/payments/methods/main.go @@ -0,0 +1,17 @@ +package main + +import ( + "github.com/tech/sendico/payments/methods/internal/appversion" + si "github.com/tech/sendico/payments/methods/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/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index 56fa2b5d..f30ae969 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -28,7 +28,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -57,8 +57,6 @@ require ( github.com/xdg-go/scram v1.2.0 // indirect github.com/xdg-go/stringprep v1.0.4 // indirect github.com/youmark/pkcs8 v0.0.0-20240726163527-a2c0da244d78 // indirect - go.opentelemetry.io/otel v1.39.0 // indirect - go.opentelemetry.io/otel/sdk/metric v1.39.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/crypto v0.48.0 // indirect diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index e6f2e0f0..07e82f8b 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -213,8 +213,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_helpers.go b/api/payments/orchestrator/internal/service/orchestrator/service_helpers.go index 38052c9f..f53833b9 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_helpers.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" + quotestorage "github.com/tech/sendico/payments/storage/quote" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" @@ -72,7 +73,7 @@ func ensurePaymentsStore(repo storage.Repository) (storage.PaymentsStore, error) return store, nil } -func ensureQuotesStore(repo storage.Repository) (storage.QuotesStore, error) { +func ensureQuotesStore(repo storage.Repository) (quotestorage.QuotesStore, error) { if repo == nil { return nil, errStorageUnavailable } diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go index 770152bc..971c01f6 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go @@ -8,6 +8,7 @@ import ( ledgerclient "github.com/tech/sendico/ledger/client" "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" + quotestorage "github.com/tech/sendico/payments/storage/quote" clockpkg "github.com/tech/sendico/pkg/clock" mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory" "github.com/tech/sendico/pkg/model/account_role" @@ -380,7 +381,7 @@ func TestInitiatePaymentByQuoteRef(t *testing.T) { type stubRepo struct { payments storage.PaymentsStore - quotes storage.QuotesStore + quotes quotestorage.QuotesStore routes storage.RoutesStore plans storage.PlanTemplatesStore pingErr error @@ -388,8 +389,11 @@ type stubRepo struct { func (s stubRepo) Ping(context.Context) error { return s.pingErr } func (s stubRepo) Payments() storage.PaymentsStore { return s.payments } -func (s stubRepo) Quotes() storage.QuotesStore { return s.quotes } -func (s stubRepo) Routes() storage.RoutesStore { return s.routes } +func (s stubRepo) PaymentMethods() storage.PaymentMethodsStore { + return nil +} +func (s stubRepo) Quotes() quotestorage.QuotesStore { return s.quotes } +func (s stubRepo) Routes() storage.RoutesStore { return s.routes } func (s stubRepo) PlanTemplates() storage.PlanTemplatesStore { if s.plans != nil { return s.plans diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_test.go index a80baf65..c2f9caa8 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_test.go @@ -10,6 +10,7 @@ import ( ledgerclient "github.com/tech/sendico/ledger/client" "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" + quotestorage "github.com/tech/sendico/payments/storage/quote" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" mo "github.com/tech/sendico/pkg/model" @@ -399,14 +400,17 @@ func TestProcessDepositObservedHandler_MatchesPayment(t *testing.T) { type stubRepository struct { store *stubPaymentsStore - quotes storage.QuotesStore + quotes quotestorage.QuotesStore routes storage.RoutesStore plans storage.PlanTemplatesStore } func (r *stubRepository) Ping(context.Context) error { return nil } func (r *stubRepository) Payments() storage.PaymentsStore { return r.store } -func (r *stubRepository) Quotes() storage.QuotesStore { +func (r *stubRepository) PaymentMethods() storage.PaymentMethodsStore { + return nil +} +func (r *stubRepository) Quotes() quotestorage.QuotesStore { if r.quotes != nil { return r.quotes } diff --git a/api/payments/quotation/go.mod b/api/payments/quotation/go.mod index 80044e13..eb998142 100644 --- a/api/payments/quotation/go.mod +++ b/api/payments/quotation/go.mod @@ -26,7 +26,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/payments/quotation/go.sum b/api/payments/quotation/go.sum index b5ad332c..c4d799eb 100644 --- a/api/payments/quotation/go.sum +++ b/api/payments/quotation/go.sum @@ -155,16 +155,16 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= @@ -213,8 +213,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/quotation/internal/service/quotation/handlers_commands.go b/api/payments/quotation/internal/service/quotation/handlers_commands.go index 86d3c5ce..2b86f4c5 100644 --- a/api/payments/quotation/internal/service/quotation/handlers_commands.go +++ b/api/payments/quotation/internal/service/quotation/handlers_commands.go @@ -11,6 +11,7 @@ import ( "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" + quotestorage "github.com/tech/sendico/payments/storage/quote" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" @@ -114,7 +115,7 @@ func (h *quotePaymentCommand) prepareQuoteCtx(req *quotationv1.QuotePaymentReque func (h *quotePaymentCommand) quotePayment( ctx context.Context, - quotesStore storage.QuotesStore, + quotesStore quotestorage.QuotesStore, qc *quoteCtx, req *quotationv1.QuotePaymentRequest, ) (*quotePaymentResult, error) { @@ -415,7 +416,7 @@ func (h *quotePaymentsCommand) prepare(req *quotationv1.QuotePaymentsRequest) (* func (h *quotePaymentsCommand) tryReuse( ctx context.Context, - quotesStore storage.QuotesStore, + quotesStore quotestorage.QuotesStore, qc *quotePaymentsCtx, ) (*model.PaymentQuoteRecord, bool, error) { @@ -515,7 +516,7 @@ func (h *quotePaymentsCommand) aggregate( func (h *quotePaymentsCommand) storeBatch( ctx context.Context, - quotesStore storage.QuotesStore, + quotesStore quotestorage.QuotesStore, qc *quotePaymentsCtx, quoteRef string, intents []*sharedv1.PaymentIntent, diff --git a/api/payments/quotation/internal/service/quotation/handlers_commands_test.go b/api/payments/quotation/internal/service/quotation/handlers_commands_test.go index 59bfb1fc..9ff93376 100644 --- a/api/payments/quotation/internal/service/quotation/handlers_commands_test.go +++ b/api/payments/quotation/internal/service/quotation/handlers_commands_test.go @@ -8,6 +8,7 @@ import ( "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" + quotestorage "github.com/tech/sendico/payments/storage/quote" "github.com/tech/sendico/pkg/merrors" mloggerfactory "github.com/tech/sendico/pkg/mlogger/factory" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" @@ -155,14 +156,15 @@ func (e *quoteCommandTestEngine) ResolvePaymentQuote(context.Context, quoteResol func (e *quoteCommandTestEngine) Repository() storage.Repository { return e.repo } type quoteCommandTestRepo struct { - quotes storage.QuotesStore + quotes quotestorage.QuotesStore } -func (r quoteCommandTestRepo) Ping(context.Context) error { return nil } -func (r quoteCommandTestRepo) Payments() storage.PaymentsStore { return nil } -func (r quoteCommandTestRepo) Quotes() storage.QuotesStore { return r.quotes } -func (r quoteCommandTestRepo) Routes() storage.RoutesStore { return nil } -func (r quoteCommandTestRepo) PlanTemplates() storage.PlanTemplatesStore { return nil } +func (r quoteCommandTestRepo) Ping(context.Context) error { return nil } +func (r quoteCommandTestRepo) Payments() storage.PaymentsStore { return nil } +func (r quoteCommandTestRepo) PaymentMethods() storage.PaymentMethodsStore { return nil } +func (r quoteCommandTestRepo) Quotes() quotestorage.QuotesStore { return r.quotes } +func (r quoteCommandTestRepo) Routes() storage.RoutesStore { return nil } +func (r quoteCommandTestRepo) PlanTemplates() storage.PlanTemplatesStore { return nil } type quoteCommandTestQuotesStore struct { byID map[string]*model.PaymentQuoteRecord diff --git a/api/payments/quotation/internal/service/quotation/service_helpers.go b/api/payments/quotation/internal/service/quotation/service_helpers.go index b738a813..fd6061ae 100644 --- a/api/payments/quotation/internal/service/quotation/service_helpers.go +++ b/api/payments/quotation/internal/service/quotation/service_helpers.go @@ -7,6 +7,7 @@ import ( "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" + quotestorage "github.com/tech/sendico/payments/storage/quote" "github.com/tech/sendico/pkg/merrors" quotationv1 "github.com/tech/sendico/pkg/proto/payments/quotation/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" @@ -42,7 +43,7 @@ func requireNonNilIntent(intent *sharedv1.PaymentIntent) error { return nil } -func ensureQuotesStore(repo storage.Repository) (storage.QuotesStore, error) { +func ensureQuotesStore(repo storage.Repository) (quotestorage.QuotesStore, error) { if repo == nil { return nil, errStorageUnavailable } diff --git a/api/payments/storage/mongo/repository.go b/api/payments/storage/mongo/repository.go index d716d268..ebc68cbd 100644 --- a/api/payments/storage/mongo/repository.go +++ b/api/payments/storage/mongo/repository.go @@ -7,11 +7,15 @@ import ( "github.com/tech/sendico/payments/storage" "github.com/tech/sendico/payments/storage/model" "github.com/tech/sendico/payments/storage/mongo/store" + quotestorage "github.com/tech/sendico/payments/storage/quote" quotemongo "github.com/tech/sendico/payments/storage/quote/mongo" + "github.com/tech/sendico/pkg/auth" "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" + "github.com/tech/sendico/pkg/mservice" + "go.mongodb.org/mongo-driver/v2/bson" ) // Store implements storage.Repository backed by MongoDB. @@ -20,13 +24,20 @@ type Store struct { ping func(context.Context) error payments storage.PaymentsStore - quotes storage.QuotesStore + methods storage.PaymentMethodsStore + quotes quotestorage.QuotesStore routes storage.RoutesStore plans storage.PlanTemplatesStore } +type paymentMethodsConfig struct { + enforcer auth.Enforcer + permissionRef bson.ObjectID +} + type options struct { - quoteRetention time.Duration + quoteRetention time.Duration + paymentMethodsAuth *paymentMethodsConfig } // Option configures the Mongo-backed payments repository. @@ -39,6 +50,16 @@ func WithQuoteRetention(retention time.Duration) Option { } } +// WithPaymentMethodsAuth enables the payment-methods store and permission checks. +func WithPaymentMethodsAuth(enforcer auth.Enforcer, permissionRef bson.ObjectID) Option { + return func(opts *options) { + opts.paymentMethodsAuth = &paymentMethodsConfig{ + enforcer: enforcer, + permissionRef: permissionRef, + } + } +} + // New constructs a Mongo-backed payments repository from a Mongo connection. func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Store, error) { if conn == nil { @@ -48,11 +69,22 @@ func New(logger mlogger.Logger, conn *db.MongoConnection, opts ...Option) (*Stor quotesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentQuoteRecord{}).Collection()) routesRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentRoute{}).Collection()) plansRepo := repository.CreateMongoRepository(conn.Database(), (&model.PaymentPlanTemplate{}).Collection()) - return NewWithRepository(logger, conn.Ping, paymentsRepo, quotesRepo, routesRepo, plansRepo, opts...) + methodsRepo := repository.CreateMongoRepository(conn.Database(), mservice.PaymentMethods) + + return newWithRepository(logger, conn.Ping, paymentsRepo, methodsRepo, quotesRepo, routesRepo, plansRepo, opts...) } // NewWithRepository constructs a payments repository using the provided primitives. func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, paymentsRepo repository.Repository, quotesRepo repository.Repository, routesRepo repository.Repository, plansRepo repository.Repository, opts ...Option) (*Store, error) { + return newWithRepository(logger, ping, paymentsRepo, nil, quotesRepo, routesRepo, plansRepo, opts...) +} + +func newWithRepository( + logger mlogger.Logger, + ping func(context.Context) error, + paymentsRepo, methodsRepo, quotesRepo, routesRepo, plansRepo repository.Repository, + opts ...Option, +) (*Store, error) { if ping == nil { return nil, merrors.InvalidArgument("payments.storage.mongo: ping func is nil") } @@ -93,10 +125,30 @@ func NewWithRepository(logger mlogger.Logger, ping func(context.Context) error, if err != nil { return nil, err } + + var methodsStore storage.PaymentMethodsStore + if cfg.paymentMethodsAuth != nil { + if methodsRepo == nil { + return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods repository is nil") + } + if cfg.paymentMethodsAuth.enforcer == nil { + return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods enforcer is nil") + } + if cfg.paymentMethodsAuth.permissionRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("payments.storage.mongo: payment methods permission reference is required") + } + + methodsStore, err = store.NewPaymentMethods(childLogger, methodsRepo, cfg.paymentMethodsAuth.enforcer, cfg.paymentMethodsAuth.permissionRef) + if err != nil { + return nil, err + } + } + result := &Store{ logger: childLogger, ping: ping, payments: paymentsStore, + methods: methodsStore, quotes: quotesRepoStore.Quotes(), routes: routesStore, plans: plansStore, @@ -118,8 +170,13 @@ func (s *Store) Payments() storage.PaymentsStore { return s.payments } +// PaymentMethods returns the payment-methods store. +func (s *Store) PaymentMethods() storage.PaymentMethodsStore { + return s.methods +} + // Quotes returns the quotes store. -func (s *Store) Quotes() storage.QuotesStore { +func (s *Store) Quotes() quotestorage.QuotesStore { return s.quotes } diff --git a/api/payments/storage/mongo/store/payment_methods.go b/api/payments/storage/mongo/store/payment_methods.go new file mode 100644 index 00000000..24ea1983 --- /dev/null +++ b/api/payments/storage/mongo/store/payment_methods.go @@ -0,0 +1,235 @@ +package store + +import ( + "context" + "errors" + + "github.com/tech/sendico/payments/storage" + "github.com/tech/sendico/pkg/auth" + "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" + pkgmodel "github.com/tech/sendico/pkg/model" + "github.com/tech/sendico/pkg/mservice" + mauth "github.com/tech/sendico/pkg/mutil/db/auth" + "go.mongodb.org/mongo-driver/v2/bson" + "go.uber.org/zap" +) + +type PaymentMethods struct { + logger mlogger.Logger + repo repository.Repository + enforcer auth.Enforcer + permissionRef bson.ObjectID +} + +// NewPaymentMethods constructs a Mongo-backed payment-methods store. +func NewPaymentMethods(logger mlogger.Logger, repo repository.Repository, enforcer auth.Enforcer, permissionRef bson.ObjectID) (*PaymentMethods, error) { + if repo == nil { + return nil, merrors.InvalidArgument("paymentMethodsStore: repository is nil") + } + if enforcer == nil { + return nil, merrors.InvalidArgument("paymentMethodsStore: enforcer is nil") + } + if permissionRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("paymentMethodsStore: permission reference is required") + } + + indexes := []*ri.Definition{ + { + Keys: []ri.Key{ + {Field: "organizationRef", Sort: ri.Asc}, + {Field: "recipientRef", Sort: ri.Asc}, + }, + }, + { + Keys: []ri.Key{{Field: "recipientRef", Sort: ri.Asc}}, + }, + } + + for _, def := range indexes { + if err := repo.CreateIndex(def); err != nil { + logger.Error("failed to ensure payment methods index", zap.Error(err), zap.String("collection", repo.Collection())) + return nil, err + } + } + + return &PaymentMethods{ + logger: logger.Named("payment_methods"), + repo: repo, + enforcer: enforcer, + permissionRef: permissionRef, + }, nil +} + +func (p *PaymentMethods) Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, method *pkgmodel.PaymentMethod) error { + if method == nil { + return merrors.InvalidArgument("paymentMethodsStore: nil payment method") + } + if accountRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: account_ref is required") + } + if organizationRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: organization_ref is required") + } + if method.RecipientRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required") + } + + if method.GetPermissionRef() == bson.NilObjectID { + method.SetPermissionRef(p.permissionRef) + } + method.SetOrganizationRef(organizationRef) + + allowed, err := p.enforcer.Enforce(ctx, method.GetPermissionRef(), accountRef, organizationRef, bson.NilObjectID, pkgmodel.ActionCreate) + if err != nil { + return err + } + if !allowed { + return merrors.AccessDenied(mservice.PaymentMethods, string(pkgmodel.ActionCreate), bson.NilObjectID) + } + + return p.repo.Insert(ctx, method, nil) +} + +func (p *PaymentMethods) Get(ctx context.Context, accountRef, methodRef bson.ObjectID) (*pkgmodel.PaymentMethod, error) { + if accountRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("paymentMethodsStore: account_ref is required") + } + if methodRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("paymentMethodsStore: method_ref is required") + } + + if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionRead); err != nil { + return nil, err + } + + method := &pkgmodel.PaymentMethod{} + if err := p.repo.Get(ctx, methodRef, method); err != nil { + return nil, err + } + return method, nil +} + +func (p *PaymentMethods) Update(ctx context.Context, accountRef bson.ObjectID, method *pkgmodel.PaymentMethod) error { + if method == nil { + return merrors.InvalidArgument("paymentMethodsStore: nil payment method") + } + if accountRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: account_ref is required") + } + if method.GetID() == nil || method.GetID().IsZero() { + return merrors.InvalidArgument("paymentMethodsStore: method id is required") + } + + if err := p.enforceObject(ctx, accountRef, *method.GetID(), pkgmodel.ActionUpdate); err != nil { + return err + } + + return p.repo.Update(ctx, method) +} + +func (p *PaymentMethods) Delete(ctx context.Context, accountRef, methodRef bson.ObjectID) error { + if accountRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: account_ref is required") + } + if methodRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: method_ref is required") + } + + if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionDelete); err != nil { + return err + } + + return p.repo.Delete(ctx, methodRef) +} + +func (p *PaymentMethods) DeleteCascade(ctx context.Context, accountRef, methodRef bson.ObjectID) error { + return p.Delete(ctx, accountRef, methodRef) +} + +func (p *PaymentMethods) SetArchived(ctx context.Context, accountRef, _ bson.ObjectID, methodRef bson.ObjectID, archived, _ bool) error { + if accountRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: account_ref is required") + } + if methodRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: method_ref is required") + } + + if err := p.enforceObject(ctx, accountRef, methodRef, pkgmodel.ActionUpdate); err != nil { + return err + } + + patch := repository.Patch().Set(repository.Field("isArchived"), archived) + return p.repo.Patch(ctx, methodRef, patch) +} + +func (p *PaymentMethods) List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *pkgmodel.ViewCursor) ([]pkgmodel.PaymentMethod, error) { + if accountRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("paymentMethodsStore: account_ref is required") + } + if organizationRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("paymentMethodsStore: organization_ref is required") + } + if recipientRef == bson.NilObjectID { + return nil, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required") + } + + items, err := mauth.GetProtectedObjects[pkgmodel.PaymentMethod]( + ctx, + p.logger, + accountRef, + organizationRef, + pkgmodel.ActionRead, + repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)), + cursor, + p.enforcer, + p.repo, + ) + if errors.Is(err, merrors.ErrNoData) { + return []pkgmodel.PaymentMethod{}, nil + } + return items, err +} + +func (p *PaymentMethods) SetArchivedByRecipient(ctx context.Context, recipientRef bson.ObjectID, archived bool) (int, error) { + if recipientRef == bson.NilObjectID { + return 0, merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required") + } + + filter := repository.Filter("recipientRef", recipientRef) + patch := repository.Patch().Set(repository.Field("isArchived"), archived) + return p.repo.PatchMany(ctx, filter, patch) +} + +func (p *PaymentMethods) DeleteByRecipient(ctx context.Context, recipientRef bson.ObjectID) error { + if recipientRef == bson.NilObjectID { + return merrors.InvalidArgument("paymentMethodsStore: recipient_ref is required") + } + return p.repo.DeleteMany(ctx, repository.Filter("recipientRef", recipientRef)) +} + +func (p *PaymentMethods) enforceObject(ctx context.Context, accountRef, methodRef bson.ObjectID, action pkgmodel.Action) error { + refs, err := p.repo.ListPermissionBound(ctx, repository.IDFilter(methodRef)) + if err != nil { + if errors.Is(err, merrors.ErrNoData) { + return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef) + } + return err + } + if len(refs) == 0 { + return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef) + } + + allowed, err := p.enforcer.Enforce(ctx, refs[0].GetPermissionRef(), accountRef, refs[0].GetOrganizationRef(), methodRef, action) + if err != nil { + return err + } + if !allowed { + return merrors.AccessDenied(mservice.PaymentMethods, string(action), methodRef) + } + return nil +} + +var _ storage.PaymentMethodsStore = (*PaymentMethods)(nil) diff --git a/api/payments/storage/storage.go b/api/payments/storage/storage.go index 2eba4e2c..4a5a9085 100644 --- a/api/payments/storage/storage.go +++ b/api/payments/storage/storage.go @@ -5,6 +5,7 @@ import ( "github.com/tech/sendico/payments/storage/model" quotestorage "github.com/tech/sendico/payments/storage/quote" + pkgmodel "github.com/tech/sendico/pkg/model" "go.mongodb.org/mongo-driver/v2/bson" ) @@ -40,6 +41,7 @@ var ( type Repository interface { Ping(ctx context.Context) error Payments() PaymentsStore + PaymentMethods() PaymentMethodsStore Quotes() quotestorage.QuotesStore Routes() RoutesStore PlanTemplates() PlanTemplatesStore @@ -55,8 +57,19 @@ type PaymentsStore interface { List(ctx context.Context, filter *model.PaymentFilter) (*model.PaymentList, error) } -// Deprecated: use quote/storage.QuotesStore. -type QuotesStore = quotestorage.QuotesStore +// PaymentMethodsStore manages recipient-linked payment methods. +type PaymentMethodsStore interface { + Create(ctx context.Context, accountRef, organizationRef bson.ObjectID, method *pkgmodel.PaymentMethod) error + Get(ctx context.Context, accountRef, methodRef bson.ObjectID) (*pkgmodel.PaymentMethod, error) + Update(ctx context.Context, accountRef bson.ObjectID, method *pkgmodel.PaymentMethod) error + Delete(ctx context.Context, accountRef, methodRef bson.ObjectID) error + DeleteCascade(ctx context.Context, accountRef, methodRef bson.ObjectID) error + SetArchived(ctx context.Context, accountRef, organizationRef, methodRef bson.ObjectID, archived, cascade bool) error + List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *pkgmodel.ViewCursor) ([]pkgmodel.PaymentMethod, error) + + SetArchivedByRecipient(ctx context.Context, recipientRef bson.ObjectID, archived bool) (int, error) + DeleteByRecipient(ctx context.Context, recipientRef bson.ObjectID) error +} // RoutesStore manages allowed routing transitions. type RoutesStore interface { diff --git a/api/pkg/auth/factory.go b/api/pkg/auth/factory.go index 9efa1c9b..3e7fb12d 100644 --- a/api/pkg/auth/factory.go +++ b/api/pkg/auth/factory.go @@ -20,6 +20,12 @@ func CreateAuth( config *Config, ) (Enforcer, Manager, error) { lg := logger.Named("auth") + if config == nil || config.Driver == "" { + lg.Warn("Permissions enforcer config is missing, defaulting to native enforcer") + config = &Config{ + Driver: Native, + } + } lg.Debug("Creating enforcer...", zap.String("driver", string(config.Driver))) l := lg.Named(string(config.Driver)) if config.Driver == Casbin { diff --git a/api/pkg/db/factory.go b/api/pkg/db/factory.go index 30fbd538..4026bab1 100644 --- a/api/pkg/db/factory.go +++ b/api/pkg/db/factory.go @@ -7,7 +7,6 @@ import ( mongoimpl "github.com/tech/sendico/pkg/db/internal/mongo" "github.com/tech/sendico/pkg/db/invitation" "github.com/tech/sendico/pkg/db/organization" - "github.com/tech/sendico/pkg/db/paymethod" "github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/db/recipient" "github.com/tech/sendico/pkg/db/refreshtokens" @@ -28,7 +27,6 @@ type Factory interface { NewOrganizationDB() (organization.DB, error) NewInvitationsDB() (invitation.DB, error) NewRecipientsDB() (recipient.DB, error) - NewPaymentMethodsDB() (paymethod.DB, error) NewVerificationsDB() (verification.DB, error) NewRolesDB() (role.DB, error) diff --git a/api/pkg/db/internal/mongo/db.go b/api/pkg/db/internal/mongo/db.go index 3a9d1785..a5ca103f 100755 --- a/api/pkg/db/internal/mongo/db.go +++ b/api/pkg/db/internal/mongo/db.go @@ -15,7 +15,6 @@ import ( "github.com/tech/sendico/pkg/db/internal/mongo/chainassetsdb" "github.com/tech/sendico/pkg/db/internal/mongo/invitationdb" "github.com/tech/sendico/pkg/db/internal/mongo/organizationdb" - "github.com/tech/sendico/pkg/db/internal/mongo/paymethoddb" "github.com/tech/sendico/pkg/db/internal/mongo/policiesdb" "github.com/tech/sendico/pkg/db/internal/mongo/recipientdb" "github.com/tech/sendico/pkg/db/internal/mongo/refreshtokensdb" @@ -24,7 +23,6 @@ import ( "github.com/tech/sendico/pkg/db/internal/mongo/verificationimp" "github.com/tech/sendico/pkg/db/invitation" "github.com/tech/sendico/pkg/db/organization" - "github.com/tech/sendico/pkg/db/paymethod" "github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/db/recipient" "github.com/tech/sendico/pkg/db/refreshtokens" @@ -204,28 +202,18 @@ func (db *DB) NewOrganizationDB() (organization.DB, error) { } func (db *DB) NewRecipientsDB() (recipient.DB, error) { - pmdb, err := db.NewPaymentMethodsDB() - if err != nil { - db.logger.Warn("Failed to create payment methods database", zap.Error(err)) - return nil, err - } - create := func(ctx context.Context, logger mlogger.Logger, enforcer auth.Enforcer, pdb policy.DB, db *mongo.Database, ) (recipient.DB, error) { - return recipientdb.Create(ctx, logger, enforcer, pdb, pmdb, db) + return recipientdb.Create(ctx, logger, enforcer, pdb, db) } return newProtectedDB(db, create) } -func (db *DB) NewPaymentMethodsDB() (paymethod.DB, error) { - return newProtectedDB(db, paymethoddb.Create) -} - func (db *DB) NewRefreshTokensDB() (refreshtokens.DB, error) { return refreshtokensdb.Create(db.logger, db.db()) } diff --git a/api/pkg/db/internal/mongo/paymethoddb/archived.go b/api/pkg/db/internal/mongo/paymethoddb/archived.go deleted file mode 100644 index d466dba6..00000000 --- a/api/pkg/db/internal/mongo/paymethoddb/archived.go +++ /dev/null @@ -1,20 +0,0 @@ -package paymethoddb - -import ( - "context" - - "github.com/tech/sendico/pkg/mutil/mzap" - "go.mongodb.org/mongo-driver/v2/bson" - "go.uber.org/zap" -) - -func (db *PaymentMethodsDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef bson.ObjectID, isArchived, cascade bool) error { - // Use the ArchivableDB for the main archiving logic - if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil { - db.DBImp.Logger.Warn("Failed to chnage object archive status", zap.Error(err), - mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef), - mzap.ObjRef("object_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade)) - return err - } - return nil -} diff --git a/api/pkg/db/internal/mongo/paymethoddb/db.go b/api/pkg/db/internal/mongo/paymethoddb/db.go deleted file mode 100644 index 03fd6656..00000000 --- a/api/pkg/db/internal/mongo/paymethoddb/db.go +++ /dev/null @@ -1,59 +0,0 @@ -package paymethoddb - -import ( - "context" - - "github.com/tech/sendico/pkg/auth" - "github.com/tech/sendico/pkg/db/policy" - ri "github.com/tech/sendico/pkg/db/repository/index" - "github.com/tech/sendico/pkg/mlogger" - "github.com/tech/sendico/pkg/model" - "github.com/tech/sendico/pkg/mservice" - "go.mongodb.org/mongo-driver/v2/mongo" - "go.uber.org/zap" -) - -type PaymentMethodsDB struct { - auth.ProtectedDBImp[*model.PaymentMethod] - auth.ArchivableDB[*model.PaymentMethod] -} - -func Create(ctx context.Context, - logger mlogger.Logger, - enforcer auth.Enforcer, - pdb policy.DB, - db *mongo.Database, -) (*PaymentMethodsDB, error) { - p, err := auth.CreateDBImp[*model.PaymentMethod](ctx, logger, pdb, enforcer, mservice.PaymentMethods, db) - if err != nil { - return nil, err - } - - createEmpty := func() *model.PaymentMethod { - return &model.PaymentMethod{} - } - - getArchivable := func(c *model.PaymentMethod) model.Archivable { - return &c.ArchivableBase - } - - res := &PaymentMethodsDB{ - ProtectedDBImp: *p, - ArchivableDB: auth.NewArchivableDB( - p.DBImp, - logger, - p.Enforcer, - createEmpty, - getArchivable, - ), - } - - if err := res.DBImp.Repository.CreateIndex(&ri.Definition{ - Keys: []ri.Key{{Field: "recipientRef", Sort: ri.Asc}}, - }); err != nil { - res.DBImp.Logger.Error("Failed to create recipientRef index for payment methods", zap.Error(err)) - return nil, err - } - - return res, nil -} diff --git a/api/pkg/db/internal/mongo/paymethoddb/list.go b/api/pkg/db/internal/mongo/paymethoddb/list.go deleted file mode 100644 index 63be7a6e..00000000 --- a/api/pkg/db/internal/mongo/paymethoddb/list.go +++ /dev/null @@ -1,28 +0,0 @@ -package paymethoddb - -import ( - "context" - "errors" - - "github.com/tech/sendico/pkg/db/repository" - "github.com/tech/sendico/pkg/merrors" - "github.com/tech/sendico/pkg/model" - mauth "github.com/tech/sendico/pkg/mutil/db/auth" - "go.mongodb.org/mongo-driver/v2/bson" -) - -func (db *PaymentMethodsDB) List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) { - res, err := mauth.GetProtectedObjects[model.PaymentMethod]( - ctx, - db.DBImp.Logger, - accountRef, organizationRef, model.ActionRead, - repository.OrgFilter(organizationRef).And(repository.Filter("recipientRef", recipientRef)), - cursor, - db.Enforcer, - db.DBImp.Repository, - ) - if errors.Is(err, merrors.ErrNoData) { - return []model.PaymentMethod{}, nil - } - return res, err -} diff --git a/api/pkg/db/internal/mongo/recipientdb/archived.go b/api/pkg/db/internal/mongo/recipientdb/archived.go index 1fd0240d..37e54532 100644 --- a/api/pkg/db/internal/mongo/recipientdb/archived.go +++ b/api/pkg/db/internal/mongo/recipientdb/archived.go @@ -2,16 +2,13 @@ package recipientdb import ( "context" - "errors" - "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mutil/mzap" "go.mongodb.org/mongo-driver/v2/bson" "go.uber.org/zap" ) func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organizationRef, objectRef bson.ObjectID, isArchived, cascade bool) error { - // Use the ArchivableDB for the main archiving logic if err := db.ArchivableDB.SetArchived(ctx, accountRef, objectRef, isArchived); err != nil { db.DBImp.Logger.Warn("Failed to change recipient archive status", zap.Error(err), mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef), @@ -19,39 +16,5 @@ func (db *RecipientDB) SetArchived(ctx context.Context, accountRef, organization return err } - if cascade { - if err := db.setArchivedPaymentMethods(ctx, accountRef, organizationRef, objectRef, isArchived); err != nil { - db.DBImp.Logger.Warn("Failed to update payment methods archive status", zap.Error(err), - mzap.AccRef(accountRef), mzap.ObjRef("organization_ref", organizationRef), - mzap.ObjRef("recipient_ref", objectRef), zap.Bool("archived", isArchived), zap.Bool("cascade", cascade)) - - return err - } - } - - return nil -} - -func (db *RecipientDB) setArchivedPaymentMethods(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, archived bool) error { - db.DBImp.Logger.Debug("Setting archived status for recipient payment methods", mzap.ObjRef("recipient_ref", recipientRef), zap.Bool("archived", archived)) - - db.DBImp.Logger.Debug("Applying archived status to payment methods for recipient", mzap.ObjRef("recipient_ref", recipientRef)) - - // Get all payMethods for the recipient - payMethods, err := db.pmdb.List(ctx, accountRef, organizationRef, recipientRef, nil) - if err != nil && !errors.Is(err, merrors.ErrNoData) { - db.DBImp.Logger.Warn("Failed to fetch payment methods for recipient", zap.Error(err), mzap.ObjRef("recipient_ref", recipientRef)) - return err - } - - // Archive each payment method - for _, pmethod := range payMethods { - if err := db.pmdb.SetArchived(ctx, accountRef, organizationRef, pmethod.ID, archived, true); err != nil { - db.DBImp.Logger.Warn("Failed to set archived status for payment method", zap.Error(err), mzap.ObjRef("payment_method_ref", pmethod.ID)) - return err - } - } - - db.DBImp.Logger.Debug("Successfully updated payment methods archived status", zap.Int("count", len(payMethods)), mzap.ObjRef("recipient_ref", recipientRef)) return nil } diff --git a/api/pkg/db/internal/mongo/recipientdb/db.go b/api/pkg/db/internal/mongo/recipientdb/db.go index 4135c73f..82b6c511 100644 --- a/api/pkg/db/internal/mongo/recipientdb/db.go +++ b/api/pkg/db/internal/mongo/recipientdb/db.go @@ -4,7 +4,6 @@ import ( "context" "github.com/tech/sendico/pkg/auth" - "github.com/tech/sendico/pkg/db/paymethod" "github.com/tech/sendico/pkg/db/policy" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" @@ -15,14 +14,12 @@ import ( type RecipientDB struct { auth.ProtectedDBImp[*model.Recipient] auth.ArchivableDB[*model.Recipient] - pmdb paymethod.DB } func Create(ctx context.Context, logger mlogger.Logger, enforcer auth.Enforcer, pdb policy.DB, - pmdb paymethod.DB, db *mongo.Database, ) (*RecipientDB, error) { p, err := auth.CreateDBImp[*model.Recipient](ctx, logger, pdb, enforcer, mservice.Recipients, db) @@ -47,7 +44,6 @@ func Create(ctx context.Context, createEmpty, getArchivable, ), - pmdb: pmdb, } return res, nil } diff --git a/api/pkg/db/paymethod/db.go b/api/pkg/db/paymethod/db.go deleted file mode 100644 index 8a915b1c..00000000 --- a/api/pkg/db/paymethod/db.go +++ /dev/null @@ -1,15 +0,0 @@ -package paymethod - -import ( - "context" - - "github.com/tech/sendico/pkg/auth" - "github.com/tech/sendico/pkg/model" - "go.mongodb.org/mongo-driver/v2/bson" -) - -type DB interface { - auth.ProtectedDB[*model.PaymentMethod] - SetArchived(ctx context.Context, accountRef, organizationRef, methodRef bson.ObjectID, archived, cascade bool) error - List(ctx context.Context, accountRef, organizationRef, recipientRef bson.ObjectID, cursor *model.ViewCursor) ([]model.PaymentMethod, error) -} diff --git a/api/pkg/go.mod b/api/pkg/go.mod index 4b252cd4..ce46df58 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -17,7 +17,7 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.48.0 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 ) @@ -81,10 +81,10 @@ require ( go.mongodb.org/mongo-driver v1.17.8 // indirect go.opentelemetry.io/auto/sdk v1.2.1 // indirect go.opentelemetry.io/contrib/instrumentation/net/http/otelhttp v0.56.0 // indirect - go.opentelemetry.io/otel v1.38.0 // indirect + go.opentelemetry.io/otel v1.39.0 // indirect go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 // indirect - go.opentelemetry.io/otel/metric v1.38.0 // indirect - go.opentelemetry.io/otel/trace v1.38.0 // indirect + go.opentelemetry.io/otel/metric v1.39.0 // indirect + go.opentelemetry.io/otel/trace v1.39.0 // indirect go.uber.org/multierr v1.11.0 // indirect go.yaml.in/yaml/v2 v2.4.3 // indirect golang.org/x/net v0.50.0 // indirect diff --git a/api/pkg/go.sum b/api/pkg/go.sum index 091b390c..cc8962ea 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -176,20 +176,20 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0 h1:Mne5On7VWdx7omSrSSZvM4Kw7cS7NQkOOmLcgscI51U= go.opentelemetry.io/otel/exporters/otlp/otlptrace v1.19.0/go.mod h1:IPtUMKL4O3tH5y+iXVyAXqpAwMuzC1IrxVS81rummfE= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0 h1:IeMeyr1aBvBiPVYihXIaeIZba6b8E1bYp7lbdxK8CQg= go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp v1.19.0/go.mod h1:oVdCUtjq9MK9BlS7TtucsQwUcXcymNiEDjgDD2jMtZU= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E= -go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg= -go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM= -go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18= +go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE= +go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8= +go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.opentelemetry.io/proto/otlp v1.0.0 h1:T0TX0tmXU8a3CbNXzEKGeU5mIVOdf0oykP+u2lIVU/I= go.opentelemetry.io/proto/otlp v1.0.0/go.mod h1:Sy6pihPLfYHkr3NkUbEhGHFhINUSI/v80hjKIs5JXpM= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= @@ -269,12 +269,12 @@ golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8T golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= 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/api v0.0.0-20251029180050-ab9386a59fda h1:+2XxjfsAu6vqFxwGBRcHiMaDCuZiqXGDUDVWVtrFAnE= -google.golang.org/genproto/googleapis/api v0.0.0-20251029180050-ab9386a59fda/go.mod h1:fDMmzKV90WSg1NbozdqrE64fkuTv6mlq2zxo9ad+3yo= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1:fCvbg86sFXwdrl5LgVcTEvNC+2txB5mgROGmRL5mrls= +google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/pkg/model/internal/notificationevent.go b/api/pkg/model/internal/notificationevent.go index 56230793..3bffd862 100644 --- a/api/pkg/model/internal/notificationevent.go +++ b/api/pkg/model/internal/notificationevent.go @@ -63,7 +63,7 @@ func FromStringImp(s string) (*NotificationEventImp, error) { func StringToNotificationAction(s string) (nm.NotificationAction, error) { switch nm.NotificationAction(s) { - case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset, nm.NAConfirmationRequest, nm.NATelegramReaction, nm.NAPaymentGatewayIntent, nm.NAPaymentGatewayExecution, nm.NADiscoveryServiceAnnounce, nm.NADiscoveryGatewayAnnounce, nm.NADiscoveryHeartbeat, nm.NADiscoveryLookupRequest, nm.NADiscoveryLookupResponse, nm.NADiscoveryRefreshUI: + case nm.NACreated, nm.NAPending, nm.NAUpdated, nm.NAArchived, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset, nm.NAConfirmationRequest, nm.NATelegramReaction, nm.NAPaymentGatewayIntent, nm.NAPaymentGatewayExecution, nm.NADiscoveryServiceAnnounce, nm.NADiscoveryGatewayAnnounce, nm.NADiscoveryHeartbeat, nm.NADiscoveryLookupRequest, nm.NADiscoveryLookupResponse, nm.NADiscoveryRefreshUI: return nm.NotificationAction(s), nil default: return "", merrors.DataConflict("invalid Notification action: " + s) diff --git a/api/pkg/model/notificationevent.go b/api/pkg/model/notificationevent.go index 2c81f4ea..995e7e39 100644 --- a/api/pkg/model/notificationevent.go +++ b/api/pkg/model/notificationevent.go @@ -78,6 +78,7 @@ func StringToNotificationAction(s string) (nm.NotificationAction, error) { case nm.NACreated, nm.NAPending, nm.NAUpdated, + nm.NAArchived, nm.NADeleted, nm.NAAssigned, nm.NAPasswordReset, diff --git a/api/pkg/mservice/services.go b/api/pkg/mservice/services.go index 0605cf09..eb6a7177 100644 --- a/api/pkg/mservice/services.go +++ b/api/pkg/mservice/services.go @@ -60,8 +60,8 @@ func StringToSType(s string) (Type, error) { case Accounts, Verification, Amplitude, Site, Changes, Clients, ChainGateway, ChainWallets, ChainWalletBalances, ChainTransfers, ChainDeposits, MntxGateway, PaymentGateway, FXOracle, FeePlans, BillingDocuments, FilterProjects, Invitations, Invoices, Logo, Ledger, LedgerAccounts, LedgerBalances, LedgerEntries, LedgerOutbox, LedgerParties, LedgerPlines, Notifications, - Organizations, Payments, PaymentRoutes, PaymentPlanTemplates, PaymentOrchestrator, Permissions, Policies, PolicyAssignements, - RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery: + Organizations, Payments, PaymentRoutes, PaymentPlanTemplates, PaymentOrchestrator, PaymentMethods, Permissions, Policies, PolicyAssignements, + Recipients, RefreshTokens, Roles, Storage, Tenants, Workflows, Discovery: return Type(s), nil default: return "", merrors.InvalidArgument("invalid service type", s) diff --git a/api/proto/payments/methods/v1/methods.proto b/api/proto/payments/methods/v1/methods.proto new file mode 100644 index 00000000..0aa05d68 --- /dev/null +++ b/api/proto/payments/methods/v1/methods.proto @@ -0,0 +1,80 @@ +syntax = "proto3"; + +package payments.methods.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/payments/methods/v1;methodsv1"; + +import "google/protobuf/wrappers.proto"; + +message ViewCursor { + google.protobuf.Int64Value limit = 1; + google.protobuf.Int64Value offset = 2; + google.protobuf.BoolValue is_archived = 3; +} + + +message CreatePaymentMethodRequest { + string account_ref = 1; + string organization_ref = 2; + bytes payment_method_json = 3; +} + +message CreatePaymentMethodResponse { + bytes payment_method_json = 1; +} + +message GetPaymentMethodRequest { + string account_ref = 1; + string payment_method_ref = 2; +} + +message GetPaymentMethodResponse { + bytes payment_method_json = 1; +} + +message UpdatePaymentMethodRequest { + string account_ref = 1; + bytes payment_method_json = 2; +} + +message UpdatePaymentMethodResponse { + bytes payment_method_json = 1; +} + +message DeletePaymentMethodRequest { + string account_ref = 1; + string payment_method_ref = 2; + bool cascade = 3; +} + +message DeletePaymentMethodResponse {} + +message SetPaymentMethodArchivedRequest { + string account_ref = 1; + string organization_ref = 2; + string payment_method_ref = 3; + bool archived = 4; + bool cascade = 5; +} + +message SetPaymentMethodArchivedResponse {} + +message ListPaymentMethodsRequest { + string account_ref = 1; + string organization_ref = 2; + string recipient_ref = 3; + ViewCursor cursor = 4; +} + +message ListPaymentMethodsResponse { + repeated bytes payment_methods_json = 1; +} + +service PaymentMethodsService { + rpc CreatePaymentMethod(CreatePaymentMethodRequest) returns (CreatePaymentMethodResponse); + rpc GetPaymentMethod(GetPaymentMethodRequest) returns (GetPaymentMethodResponse); + rpc UpdatePaymentMethod(UpdatePaymentMethodRequest) returns (UpdatePaymentMethodResponse); + rpc DeletePaymentMethod(DeletePaymentMethodRequest) returns (DeletePaymentMethodResponse); + rpc SetPaymentMethodArchived(SetPaymentMethodArchivedRequest) returns (SetPaymentMethodArchivedResponse); + rpc ListPaymentMethods(ListPaymentMethodsRequest) returns (ListPaymentMethodsResponse); +} diff --git a/api/server/config.dev.yml b/api/server/config.dev.yml index 97a05aeb..bae06ea4 100755 --- a/api/server/config.dev.yml +++ b/api/server/config.dev.yml @@ -103,6 +103,12 @@ api: dial_timeout_seconds: 5 call_timeout_seconds: 5 insecure: true + payment_methods: + address: dev-payments-methods:50066 + address_env: PAYMENTS_METHODS_ADDRESS + dial_timeout_seconds: 5 + call_timeout_seconds: 5 + insecure: true app: diff --git a/api/server/config.yml b/api/server/config.yml index 1a424b75..42440297 100755 --- a/api/server/config.yml +++ b/api/server/config.yml @@ -103,6 +103,12 @@ api: dial_timeout_seconds: 5 call_timeout_seconds: 5 insecure: true + payment_methods: + address: sendico_payments_methods:50066 + address_env: PAYMENTS_METHODS_ADDRESS + dial_timeout_seconds: 5 + call_timeout_seconds: 5 + insecure: true app: diff --git a/api/server/go.mod b/api/server/go.mod index 5e52e34f..4a5ab6bd 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -8,6 +8,8 @@ replace github.com/tech/sendico/ledger => ../ledger replace github.com/tech/sendico/payments/orchestrator => ../payments/orchestrator +replace github.com/tech/sendico/payments/methods => ../payments/methods + replace github.com/tech/sendico/payments/storage => ../payments/storage replace github.com/tech/sendico/gateway/tron => ../gateway/tron @@ -27,6 +29,7 @@ require ( github.com/stretchr/testify v1.11.1 github.com/tech/sendico/gateway/tron v0.0.0-00010101000000-000000000000 github.com/tech/sendico/ledger v0.0.0-00010101000000-000000000000 + github.com/tech/sendico/payments/methods v0.0.0-00010101000000-000000000000 github.com/tech/sendico/payments/orchestrator v0.0.0-00010101000000-000000000000 github.com/tech/sendico/pkg v0.1.0 github.com/testcontainers/testcontainers-go v0.33.0 @@ -34,7 +37,7 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 golang.org/x/net v0.50.0 - google.golang.org/grpc v1.78.0 + google.golang.org/grpc v1.79.0 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 moul.io/chizap v1.0.3 diff --git a/api/server/go.sum b/api/server/go.sum index 79826cbb..eb538f72 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -365,8 +365,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.78.0 h1:K1XZG/yGDJnzMdd/uZHAkVqJE+xIDOcmdSFZkBUicNc= -google.golang.org/grpc v1.78.0/go.mod h1:I47qjTo4OKbMkjA/aOOwxDIiPSBofUtQUI5EfpWvW7U= +google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= +google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/server/interface/api/config.go b/api/server/interface/api/config.go index a7c7f5bc..0f7898b7 100644 --- a/api/server/interface/api/config.go +++ b/api/server/interface/api/config.go @@ -12,6 +12,7 @@ type Config struct { Ledger *LedgerConfig `yaml:"ledger"` PaymentOrchestrator *PaymentOrchestratorConfig `yaml:"payment_orchestrator"` PaymentQuotation *PaymentOrchestratorConfig `yaml:"payment_quotation"` + PaymentMethods *PaymentOrchestratorConfig `yaml:"payment_methods"` } type ChainGatewayConfig struct { diff --git a/api/server/internal/api/discovery_resolver.go b/api/server/internal/api/discovery_resolver.go index f9e4da72..c3876691 100644 --- a/api/server/internal/api/discovery_resolver.go +++ b/api/server/internal/api/discovery_resolver.go @@ -27,6 +27,7 @@ const ( ledgerDebitOperation = "ledger.debit" ledgerCreditOperation = "ledger.credit" gatewayReadBalanceOperation = "balance.read" + paymentMethodsReadOperation = "payment_methods.read" ) var ( @@ -44,6 +45,11 @@ var ( "PAYMENT_QUOTATION", "payment_quotation", } + paymentMethodsDiscoveryServiceNames = []string{ + "PAYMENTS_METHODS", + "PAYMENT_METHODS", + string(mservice.PaymentMethods), + } ) type discoveryEndpoint struct { @@ -105,6 +111,7 @@ func (a *APIImp) resolveServiceAddressesFromDiscovery() { orchestratorFound, orchestratorEndpoint := a.resolvePaymentOrchestratorAddress(lookup.Services) a.resolveLedgerAddress(lookup.Services) a.resolvePaymentQuotationAddress(lookup.Services, orchestratorFound, orchestratorEndpoint) + a.resolvePaymentMethodsAddress(lookup.Services) } func (a *APIImp) resolveChainGatewayAddress(gateways []discovery.GatewaySummary) { @@ -218,6 +225,30 @@ func (a *APIImp) resolvePaymentQuotationAddress(services []discovery.ServiceSumm zap.Bool("insecure", endpoint.insecure)) } +func (a *APIImp) resolvePaymentMethodsAddress(services []discovery.ServiceSummary) { + endpoint, selected, ok := selectServiceEndpoint( + services, + paymentMethodsDiscoveryServiceNames, + []string{paymentMethodsReadOperation}, + ) + if !ok { + return + } + + cfg := ensurePaymentMethodsConfig(a.config) + cfg.Address = endpoint.address + cfg.Insecure = endpoint.insecure + ensureTimeoutsPayment(cfg) + + a.logger.Info("Resolved payment methods address from discovery", + zap.String("service", selected.Service), + zap.String("service_id", selected.ID), + zap.String("instance_id", selected.InstanceID), + zap.String("invoke_uri", endpoint.raw), + zap.String("address", endpoint.address), + zap.Bool("insecure", endpoint.insecure)) +} + func selectServiceEndpoint(services []discovery.ServiceSummary, serviceNames []string, requiredOps []string) (discoveryEndpoint, discovery.ServiceSummary, bool) { selections := make([]serviceSelection, 0) for _, svc := range services { @@ -429,6 +460,16 @@ func ensurePaymentQuotationConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorCon return cfg.PaymentQuotation } +func ensurePaymentMethodsConfig(cfg *eapi.Config) *eapi.PaymentOrchestratorConfig { + if cfg == nil { + return nil + } + if cfg.PaymentMethods == nil { + cfg.PaymentMethods = &eapi.PaymentOrchestratorConfig{} + } + return cfg.PaymentMethods +} + func ensureTimeoutsLedger(cfg *eapi.LedgerConfig) { if cfg == nil { return diff --git a/api/server/internal/server/accountapiimp/service.go b/api/server/internal/server/accountapiimp/service.go index 76bdfba7..53503589 100644 --- a/api/server/internal/server/accountapiimp/service.go +++ b/api/server/internal/server/accountapiimp/service.go @@ -7,6 +7,7 @@ import ( "strings" "time" + ledgerclient "github.com/tech/sendico/ledger/client" trongatewayclient "github.com/tech/sendico/gateway/tron/client" api "github.com/tech/sendico/pkg/api/http" "github.com/tech/sendico/pkg/auth" @@ -22,6 +23,7 @@ import ( "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/mservice" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "github.com/tech/sendico/server/interface/accountservice" eapi "github.com/tech/sendico/server/interface/api" "github.com/tech/sendico/server/interface/services/fileservice" @@ -49,6 +51,7 @@ type AccountAPI struct { accountsPermissionRef bson.ObjectID accService accountservice.AccountService chainGateway chainWalletClient + ledgerClient ledgerAccountClient chainAsset *chainv1.Asset } @@ -57,6 +60,11 @@ type chainWalletClient interface { Close() error } +type ledgerAccountClient interface { + CreateAccount(ctx context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) + Close() error +} + func (a *AccountAPI) Name() mservice.Type { return mservice.Accounts } @@ -70,6 +78,11 @@ func (a *AccountAPI) Finish(ctx context.Context) error { a.logger.Warn("Failed to close chain gateway client", zap.Error(err)) } } + if a.ledgerClient != nil { + if err := a.ledgerClient.Close(); err != nil { + a.logger.Warn("Failed to close ledger client", zap.Error(err)) + } + } return nil } @@ -158,6 +171,10 @@ func CreateAPI(a eapi.API) (*AccountAPI, error) { p.logger.Error("Failed to initialize chain gateway client", zap.Error(err)) return nil, err } + if err := p.initLedgerClient(cfg.Ledger); err != nil { + p.logger.Error("Failed to initialize ledger client", zap.Error(err)) + return nil, err + } return p, nil } @@ -198,6 +215,35 @@ func (a *AccountAPI) initChainGateway(cfg *eapi.ChainGatewayConfig) error { return nil } +func (a *AccountAPI) initLedgerClient(cfg *eapi.LedgerConfig) error { + if cfg == nil { + return merrors.InvalidArgument("ledger configuration is not provided") + } + + address := strings.TrimSpace(cfg.Address) + if address == "" { + address = strings.TrimSpace(os.Getenv(cfg.AddressEnv)) + } + if address == "" { + return merrors.InvalidArgument(fmt.Sprintf("ledger address is not specified and address env %s is empty", cfg.AddressEnv)) + } + + clientCfg := ledgerclient.Config{ + Address: address, + DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second, + CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second, + Insecure: cfg.Insecure, + } + + client, err := ledgerclient.New(context.Background(), clientCfg) + if err != nil { + return err + } + + a.ledgerClient = client + return nil +} + func buildGatewayAsset(cfg eapi.ChainGatewayAssetConfig) (*chainv1.Asset, error) { chain, err := parseChainNetwork(cfg.Chain) if err != nil { diff --git a/api/server/internal/server/accountapiimp/signup.go b/api/server/internal/server/accountapiimp/signup.go index 1f850bba..9d1e22c1 100644 --- a/api/server/internal/server/accountapiimp/signup.go +++ b/api/server/internal/server/accountapiimp/signup.go @@ -19,6 +19,7 @@ import ( "github.com/tech/sendico/pkg/mutil/mzap" describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" "github.com/tech/sendico/server/interface/api/srequest" "github.com/tech/sendico/server/interface/api/sresponse" "go.mongodb.org/mongo-driver/v2/bson" @@ -199,6 +200,10 @@ func (a *AccountAPI) signupTransactionBody(ctx context.Context, sr *srequest.Sig return nil, err } a.logger.Info("Organization wallet created", mzap.StorableRef(org), zap.String("account", sr.Account.Login)) + if err := a.openOrgLedgerAccount(ctx, org, sr); err != nil { + return nil, err + } + a.logger.Info("Organization ledger account created", mzap.StorableRef(org), zap.String("account", sr.Account.Login)) roleDescription, err := a.pmanager.Role().Create(ctx, org.ID, &sr.OwnerRole) if err != nil { @@ -323,3 +328,57 @@ func (a *AccountAPI) openOrgWallet(ctx context.Context, org *model.Organization, a.logger.Info("Managed wallet created for organization", mzap.StorableRef(org), zap.String("wallet_ref", resp.Wallet.WalletRef)) return nil } + +func (a *AccountAPI) openOrgLedgerAccount(ctx context.Context, org *model.Organization, sr *srequest.Signup) error { + if a.ledgerClient == nil { + a.logger.Warn("Ledger client not configured, skipping ledger account creation", mzap.StorableRef(org)) + return merrors.Internal("ledger client is not configured") + } + if a.chainAsset == nil { + return merrors.Internal("chain gateway default asset is not configured") + } + + currency := strings.ToUpper(strings.TrimSpace(a.chainAsset.TokenSymbol)) + if currency == "" { + return merrors.Internal("chain gateway default asset token symbol is not configured") + } + + var describable *describablev1.Describable + name := strings.TrimSpace(sr.LedgerWallet.Name) + var description *string + if sr.LedgerWallet.Description != nil { + trimmed := strings.TrimSpace(*sr.LedgerWallet.Description) + if trimmed != "" { + description = &trimmed + } + } + if name != "" || description != nil { + describable = &describablev1.Describable{ + Name: name, + Description: description, + } + } + + resp, err := a.ledgerClient.CreateAccount(ctx, &ledgerv1.CreateAccountRequest{ + OrganizationRef: org.ID.Hex(), + AccountType: ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, + Currency: currency, + Status: ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, + Role: ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, + Metadata: map[string]string{ + "source": "signup", + "login": sr.Account.Login, + }, + Describable: describable, + }) + if err != nil { + a.logger.Warn("Failed to create ledger account for organization", zap.Error(err), mzap.StorableRef(org)) + return err + } + if resp == nil || resp.GetAccount() == nil || strings.TrimSpace(resp.GetAccount().GetLedgerAccountRef()) == "" { + return merrors.Internal("ledger returned empty account reference") + } + + a.logger.Info("Ledger account created for organization", mzap.StorableRef(org), zap.String("ledger_account_ref", resp.GetAccount().GetLedgerAccountRef())) + return nil +} diff --git a/api/server/internal/server/accountapiimp/signup_ledger_test.go b/api/server/internal/server/accountapiimp/signup_ledger_test.go new file mode 100644 index 00000000..d6981a98 --- /dev/null +++ b/api/server/internal/server/accountapiimp/signup_ledger_test.go @@ -0,0 +1,116 @@ +package accountapiimp + +import ( + "context" + "errors" + "testing" + + "github.com/stretchr/testify/assert" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/model" + chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" + ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" + "github.com/tech/sendico/server/interface/api/srequest" + "go.mongodb.org/mongo-driver/v2/bson" + "go.uber.org/zap" +) + +type stubLedgerAccountClient struct { + createReq *ledgerv1.CreateAccountRequest + createResp *ledgerv1.CreateAccountResponse + createErr error +} + +func (s *stubLedgerAccountClient) CreateAccount(_ context.Context, req *ledgerv1.CreateAccountRequest) (*ledgerv1.CreateAccountResponse, error) { + s.createReq = req + return s.createResp, s.createErr +} + +func (s *stubLedgerAccountClient) Close() error { + return nil +} + +func TestOpenOrgLedgerAccount(t *testing.T) { + t.Run("creates operating ledger account", func(t *testing.T) { + desc := " Main org ledger account " + sr := &srequest.Signup{ + Account: model.AccountData{ + LoginData: model.LoginData{ + UserDataBase: model.UserDataBase{ + Login: "owner@example.com", + }, + }, + }, + LedgerWallet: model.Describable{ + Name: " Primary Ledger ", + Description: &desc, + }, + } + + org := &model.Organization{} + org.SetID(bson.NewObjectID()) + + ledgerStub := &stubLedgerAccountClient{ + createResp: &ledgerv1.CreateAccountResponse{ + Account: &ledgerv1.LedgerAccount{LedgerAccountRef: bson.NewObjectID().Hex()}, + }, + } + api := &AccountAPI{ + logger: zap.NewNop(), + ledgerClient: ledgerStub, + chainAsset: &chainv1.Asset{ + TokenSymbol: " usdt ", + }, + } + + err := api.openOrgLedgerAccount(context.Background(), org, sr) + assert.NoError(t, err) + if assert.NotNil(t, ledgerStub.createReq) { + assert.Equal(t, org.ID.Hex(), ledgerStub.createReq.GetOrganizationRef()) + assert.Equal(t, "USDT", ledgerStub.createReq.GetCurrency()) + assert.Equal(t, ledgerv1.AccountType_ACCOUNT_TYPE_ASSET, ledgerStub.createReq.GetAccountType()) + assert.Equal(t, ledgerv1.AccountStatus_ACCOUNT_STATUS_ACTIVE, ledgerStub.createReq.GetStatus()) + assert.Equal(t, ledgerv1.AccountRole_ACCOUNT_ROLE_OPERATING, ledgerStub.createReq.GetRole()) + assert.Equal(t, map[string]string{ + "source": "signup", + "login": "owner@example.com", + }, ledgerStub.createReq.GetMetadata()) + if assert.NotNil(t, ledgerStub.createReq.GetDescribable()) { + assert.Equal(t, "Primary Ledger", ledgerStub.createReq.GetDescribable().GetName()) + if assert.NotNil(t, ledgerStub.createReq.GetDescribable().Description) { + assert.Equal(t, "Main org ledger account", ledgerStub.createReq.GetDescribable().GetDescription()) + } + } + } + }) + + t.Run("fails when ledger client is missing", func(t *testing.T) { + api := &AccountAPI{ + logger: zap.NewNop(), + chainAsset: &chainv1.Asset{ + TokenSymbol: "USDT", + }, + } + + err := api.openOrgLedgerAccount(context.Background(), &model.Organization{}, &srequest.Signup{}) + assert.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInternal)) + }) + + t.Run("fails when ledger response has empty reference", func(t *testing.T) { + ledgerStub := &stubLedgerAccountClient{ + createResp: &ledgerv1.CreateAccountResponse{}, + } + api := &AccountAPI{ + logger: zap.NewNop(), + ledgerClient: ledgerStub, + chainAsset: &chainv1.Asset{ + TokenSymbol: "USDT", + }, + } + + err := api.openOrgLedgerAccount(context.Background(), &model.Organization{}, &srequest.Signup{}) + assert.Error(t, err) + assert.True(t, errors.Is(err, merrors.ErrInternal)) + }) +} diff --git a/api/server/internal/server/paymethodsimp/service.go b/api/server/internal/server/paymethodsimp/service.go index 7b4147a7..538e3d08 100644 --- a/api/server/internal/server/paymethodsimp/service.go +++ b/api/server/internal/server/paymethodsimp/service.go @@ -2,45 +2,352 @@ package paymethodsimp import ( "context" + "encoding/json" + "io" + "net/http" + "os" + "strings" + "time" - "github.com/tech/sendico/pkg/db/paymethod" + methodsclient "github.com/tech/sendico/payments/methods/client" + api "github.com/tech/sendico/pkg/api/http" + "github.com/tech/sendico/pkg/api/http/response" + "github.com/tech/sendico/pkg/merrors" + "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" + methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" eapi "github.com/tech/sendico/server/interface/api" - "github.com/tech/sendico/server/internal/server/papitemplate" - "go.uber.org/zap" + "github.com/tech/sendico/server/interface/api/sresponse" + mutil "github.com/tech/sendico/server/internal/mutil/param" + "google.golang.org/grpc/codes" + "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/wrapperspb" ) -type RecipientAPI struct { - papitemplate.ProtectedAPI[model.PaymentMethod] - db paymethod.DB +type PaymentMethodsAPI struct { + logger mlogger.Logger + client methodsclient.Client + oph mutil.ParamHelper + rph mutil.ParamHelper + mph mutil.ParamHelper } -func (a *RecipientAPI) Name() mservice.Type { +func (a *PaymentMethodsAPI) Name() mservice.Type { return mservice.PaymentMethods } -func (a *RecipientAPI) Finish(_ context.Context) error { +func (a *PaymentMethodsAPI) Finish(_ context.Context) error { + if a.client != nil { + return a.client.Close() + } return nil } -func CreateAPI(a eapi.API) (*RecipientAPI, error) { - dbFactory := func() (papitemplate.ProtectedDB[model.PaymentMethod], error) { - return a.DBFactory().NewPaymentMethodsDB() +func CreateAPI(apiCtx eapi.API) (*PaymentMethodsAPI, error) { + logger := apiCtx.Logger().Named(mservice.PaymentMethods) + + cfg := apiCtx.Config().PaymentMethods + if cfg == nil { + return nil, merrors.InvalidArgument("payment methods configuration is not provided") } - res := &RecipientAPI{} - - p, err := papitemplate.CreateAPI(a, dbFactory, mservice.Recipients, mservice.PaymentMethods) + address, err := resolveClientAddress("payment methods", cfg) if err != nil { return nil, err } - res.ProtectedAPI = *p.Build() - if res.db, err = a.DBFactory().NewPaymentMethodsDB(); err != nil { - res.Logger.Warn("Failed to create payment methods database", zap.Error(err)) + clientCfg := methodsclient.Config{ + Address: address, + DialTimeout: time.Duration(cfg.DialTimeoutSeconds) * time.Second, + CallTimeout: time.Duration(cfg.CallTimeoutSeconds) * time.Second, + Insecure: cfg.Insecure, + } + + client, err := methodsclient.New(context.Background(), clientCfg) + if err != nil { return nil, err } + res := &PaymentMethodsAPI{ + logger: logger, + client: client, + oph: mutil.CreatePH(mservice.Organizations), + rph: mutil.CreatePH(mservice.Recipients), + mph: mutil.CreatePH(mservice.PaymentMethods), + } + + apiCtx.Register().AccountHandler(res.Name(), res.oph.AddRef("/"), api.Post, res.create) + apiCtx.Register().AccountHandler(res.Name(), res.rph.AddRef(res.oph.AddRef("/list")), api.Get, res.list) + apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef("/"), api.Get, res.get) + apiCtx.Register().AccountHandler(res.Name(), "/", api.Put, res.update) + apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef("/"), api.Delete, res.delete) + apiCtx.Register().AccountHandler(res.Name(), res.mph.AddRef(res.oph.AddRef("/archive")), api.Get, res.archive) + return res, nil } + +func (a *PaymentMethodsAPI) create(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { + orgRef, err := a.oph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err) + } + + payload, err := io.ReadAll(r.Body) + if err != nil { + return response.BadPayload(a.logger, a.Name(), err) + } + + resp, err := a.client.CreatePaymentMethod(r.Context(), &methodsv1.CreatePaymentMethodRequest{ + AccountRef: account.ID.Hex(), + OrganizationRef: orgRef.Hex(), + PaymentMethodJson: payload, + }) + if err != nil { + return grpcErrorResponse(a.logger, a.Name(), err) + } + + pm, err := decodePaymentMethod(resp.GetPaymentMethodJson()) + if err != nil { + return response.Internal(a.logger, a.Name(), err) + } + return sresponse.ObjectAuthCreated(a.logger, pm, token, a.Name()) +} + +func (a *PaymentMethodsAPI) list(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { + orgRef, err := a.oph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err) + } + recipientRef, err := a.rph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.rph.Name(), a.rph.GetID(r), err) + } + + cursor, err := mutil.GetViewCursor(a.logger, r) + if err != nil { + return response.Auto(a.logger, a.Name(), err) + } + + resp, err := a.client.ListPaymentMethods(r.Context(), &methodsv1.ListPaymentMethodsRequest{ + AccountRef: account.ID.Hex(), + OrganizationRef: orgRef.Hex(), + RecipientRef: recipientRef.Hex(), + Cursor: toProtoCursor(cursor), + }) + if err != nil { + return grpcErrorResponse(a.logger, a.Name(), err) + } + + items, err := decodePaymentMethods(resp.GetPaymentMethodsJson()) + if err != nil { + return response.Internal(a.logger, a.Name(), err) + } + return sresponse.ObjectsAuth(a.logger, items, token, a.Name()) +} + +func (a *PaymentMethodsAPI) get(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { + methodRef, err := a.mph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err) + } + + resp, err := a.client.GetPaymentMethod(r.Context(), &methodsv1.GetPaymentMethodRequest{ + AccountRef: account.ID.Hex(), + PaymentMethodRef: methodRef.Hex(), + }) + if err != nil { + return grpcErrorResponse(a.logger, a.Name(), err) + } + + pm, err := decodePaymentMethod(resp.GetPaymentMethodJson()) + if err != nil { + return response.Internal(a.logger, a.Name(), err) + } + return sresponse.ObjectAuth(a.logger, pm, token, a.Name()) +} + +func (a *PaymentMethodsAPI) update(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { + payload, err := io.ReadAll(r.Body) + if err != nil { + return response.BadPayload(a.logger, a.Name(), err) + } + + resp, err := a.client.UpdatePaymentMethod(r.Context(), &methodsv1.UpdatePaymentMethodRequest{ + AccountRef: account.ID.Hex(), + PaymentMethodJson: payload, + }) + if err != nil { + return grpcErrorResponse(a.logger, a.Name(), err) + } + + pm, err := decodePaymentMethod(resp.GetPaymentMethodJson()) + if err != nil { + return response.Internal(a.logger, a.Name(), err) + } + return sresponse.ObjectAuth(a.logger, pm, token, a.Name()) +} + +func (a *PaymentMethodsAPI) delete(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { + methodRef, err := a.mph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err) + } + + cascade, err := mutil.GetCascadeParam(a.logger, r) + if err != nil { + return response.Auto(a.logger, a.Name(), err) + } + + cascadeValue := false + if cascade != nil { + cascadeValue = *cascade + } + + _, err = a.client.DeletePaymentMethod(r.Context(), &methodsv1.DeletePaymentMethodRequest{ + AccountRef: account.ID.Hex(), + PaymentMethodRef: methodRef.Hex(), + Cascade: cascadeValue, + }) + if err != nil { + return grpcErrorResponse(a.logger, a.Name(), err) + } + + return sresponse.ObjectsAuth(a.logger, []model.PaymentMethod{}, token, a.Name()) +} + +func (a *PaymentMethodsAPI) archive(r *http.Request, account *model.Account, token *sresponse.TokenData) http.HandlerFunc { + methodRef, err := a.mph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.mph.Name(), a.mph.GetID(r), err) + } + orgRef, err := a.oph.GetRef(r) + if err != nil { + return response.BadReference(a.logger, a.Name(), a.oph.Name(), a.oph.GetID(r), err) + } + + archived, err := mutil.GetArchiveParam(a.logger, r) + if err != nil { + return response.Auto(a.logger, a.Name(), err) + } + if archived == nil { + return response.BadRequest(a.logger, a.Name(), "invalid_query_parameter", "'archived' param must be present") + } + + cascade, err := mutil.GetCascadeParam(a.logger, r) + if err != nil { + return response.Auto(a.logger, a.Name(), err) + } + cascadeValue := false + if cascade != nil { + cascadeValue = *cascade + } + + _, err = a.client.SetPaymentMethodArchived(r.Context(), &methodsv1.SetPaymentMethodArchivedRequest{ + AccountRef: account.ID.Hex(), + OrganizationRef: orgRef.Hex(), + PaymentMethodRef: methodRef.Hex(), + Archived: *archived, + Cascade: cascadeValue, + }) + if err != nil { + return grpcErrorResponse(a.logger, a.Name(), err) + } + + return sresponse.ObjectsAuth(a.logger, []model.PaymentMethod{}, token, a.Name()) +} + +func resolveClientAddress(service string, cfg *eapi.PaymentOrchestratorConfig) (string, error) { + if cfg == nil { + return "", merrors.InvalidArgument(strings.TrimSpace(service) + " configuration is not provided") + } + address := strings.TrimSpace(cfg.Address) + if address != "" { + return address, nil + } + if env := strings.TrimSpace(cfg.AddressEnv); env != "" { + if resolved := strings.TrimSpace(os.Getenv(env)); resolved != "" { + return resolved, nil + } + return "", merrors.InvalidArgument(service + " address is not specified and address env " + env + " is empty") + } + return "", merrors.InvalidArgument(strings.TrimSpace(service) + " address is not specified") +} + +func toProtoCursor(cursor *model.ViewCursor) *methodsv1.ViewCursor { + if cursor == nil { + return nil + } + + res := &methodsv1.ViewCursor{} + hasAny := false + if cursor.Limit != nil { + res.Limit = wrapperspb.Int64(*cursor.Limit) + hasAny = true + } + if cursor.Offset != nil { + res.Offset = wrapperspb.Int64(*cursor.Offset) + hasAny = true + } + if cursor.IsArchived != nil { + res.IsArchived = wrapperspb.Bool(*cursor.IsArchived) + hasAny = true + } + if !hasAny { + return nil + } + return res +} + +func decodePaymentMethod(payload []byte) (*model.PaymentMethod, error) { + var pm model.PaymentMethod + if err := json.Unmarshal(payload, &pm); err != nil { + return nil, err + } + return &pm, nil +} + +func decodePaymentMethods(items [][]byte) ([]model.PaymentMethod, error) { + if len(items) == 0 { + return nil, nil + } + res := make([]model.PaymentMethod, 0, len(items)) + for i := range items { + pm, err := decodePaymentMethod(items[i]) + if err != nil { + return nil, err + } + res = append(res, *pm) + } + return res, nil +} + +func grpcErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc { + statusErr, ok := status.FromError(err) + if !ok { + return response.Internal(logger, source, err) + } + + switch statusErr.Code() { + case codes.InvalidArgument: + return response.BadRequest(logger, source, "invalid_argument", statusErr.Message()) + case codes.NotFound: + return response.NotFound(logger, source, statusErr.Message()) + case codes.PermissionDenied: + return response.AccessDenied(logger, source, statusErr.Message()) + case codes.Unauthenticated: + return response.Unauthorized(logger, source, statusErr.Message()) + case codes.AlreadyExists, codes.Aborted: + return response.DataConflict(logger, source, statusErr.Message()) + case codes.Unimplemented: + return response.NotImplemented(logger, source, statusErr.Message()) + case codes.FailedPrecondition: + return response.Error(logger, source, http.StatusPreconditionFailed, "failed_precondition", statusErr.Message()) + case codes.DeadlineExceeded: + return response.Error(logger, source, http.StatusGatewayTimeout, "deadline_exceeded", statusErr.Message()) + case codes.Unavailable: + return response.Error(logger, source, http.StatusServiceUnavailable, "service_unavailable", statusErr.Message()) + default: + return response.Internal(logger, source, err) + } +} diff --git a/api/server/internal/server/recipientimp/notifications.go b/api/server/internal/server/recipientimp/notifications.go new file mode 100644 index 00000000..b3709824 --- /dev/null +++ b/api/server/internal/server/recipientimp/notifications.go @@ -0,0 +1,22 @@ +package recipientimp + +import ( + messaging "github.com/tech/sendico/pkg/messaging/envelope" + notifications "github.com/tech/sendico/pkg/messaging/notifications/object" + "github.com/tech/sendico/pkg/model" + nm "github.com/tech/sendico/pkg/model/notification" + "go.mongodb.org/mongo-driver/v2/bson" +) + +func (a *RecipientAPI) notification( + recipient *model.Recipient, + actorAccountRef bson.ObjectID, + t nm.NotificationAction, +) messaging.Envelope { + objectRef := bson.NilObjectID + if recipient != nil { + objectRef = recipient.ID + } + + return notifications.Object(a.Name(), actorAccountRef, a.Name(), objectRef, t) +} diff --git a/api/server/internal/server/recipientimp/service.go b/api/server/internal/server/recipientimp/service.go index d2b678bf..8dadd672 100644 --- a/api/server/internal/server/recipientimp/service.go +++ b/api/server/internal/server/recipientimp/service.go @@ -35,7 +35,11 @@ func CreateAPI(a eapi.API) (*RecipientAPI, error) { if err != nil { return nil, err } - res.ProtectedAPI = *p.Build() + res.ProtectedAPI = *p. + WithNotifications(res.notification). + WithNoCreateNotification(). + WithNoUpdateNotification(). + Build() if res.db, err = a.DBFactory().NewRecipientsDB(); err != nil { res.Logger.Warn("Failed to create recipients database", zap.Error(err)) diff --git a/ci/dev/bff.dockerfile b/ci/dev/bff.dockerfile index 8fe4f6dd..e1cb3298 100644 --- a/ci/dev/bff.dockerfile +++ b/ci/dev/bff.dockerfile @@ -17,6 +17,7 @@ RUN bash ci/scripts/proto/generate.sh # Copy service dependencies (needed for go.mod replace directives) COPY api/ledger ./api/ledger COPY api/payments/orchestrator ./api/payments/orchestrator +COPY api/payments/methods ./api/payments/methods COPY api/payments/storage ./api/payments/storage COPY api/gateway/tron ./api/gateway/tron COPY api/billing/fees ./api/billing/fees @@ -39,6 +40,7 @@ COPY --from=builder /src/api/pkg ./api/pkg # Copy service dependencies COPY --from=builder /src/api/ledger ./api/ledger COPY --from=builder /src/api/payments/orchestrator ./api/payments/orchestrator +COPY --from=builder /src/api/payments/methods ./api/payments/methods COPY --from=builder /src/api/payments/storage ./api/payments/storage COPY --from=builder /src/api/gateway/tron ./api/gateway/tron COPY --from=builder /src/api/billing/fees ./api/billing/fees diff --git a/ci/dev/payments-methods.dockerfile b/ci/dev/payments-methods.dockerfile new file mode 100644 index 00000000..f7649895 --- /dev/null +++ b/ci/dev/payments-methods.dockerfile @@ -0,0 +1,40 @@ +# Development Dockerfile for Payments Methods Service with Air hot reload + +FROM golang:alpine AS builder + +RUN apk add --no-cache bash git build-base protoc protobuf-dev && \ + go install google.golang.org/protobuf/cmd/protoc-gen-go@latest && \ + go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest && \ + go install github.com/air-verse/air@latest + +WORKDIR /src + +COPY api/proto ./api/proto +COPY api/pkg ./api/pkg +COPY ci/scripts/proto/generate.sh ./ci/scripts/proto/ +RUN bash ci/scripts/proto/generate.sh + +# Copy service dependencies (needed for go.mod replace directives) +COPY api/payments/storage ./api/payments/storage + +# Runtime stage for development with Air +FROM golang:alpine + +RUN apk add --no-cache bash git build-base && \ + go install github.com/air-verse/air@latest + +WORKDIR /src + +# Copy generated proto and pkg from builder +COPY --from=builder /src/api/proto ./api/proto +COPY --from=builder /src/api/pkg ./api/pkg + +# Copy service dependencies +COPY --from=builder /src/api/payments/storage ./api/payments/storage + +# Source code will be mounted at runtime +WORKDIR /src/api/payments/methods + +EXPOSE 50066 9416 + +CMD ["air", "-c", ".air.toml", "--", "-config.file", "/app/config.yml", "-debug"] diff --git a/ci/prod/.env.runtime b/ci/prod/.env.runtime index 7a314669..268cd7e0 100644 --- a/ci/prod/.env.runtime +++ b/ci/prod/.env.runtime @@ -148,6 +148,13 @@ PAYMENTS_QUOTATION_SERVICE_NAME=sendico_payments_quotation PAYMENTS_QUOTATION_GRPC_PORT=50064 PAYMENTS_QUOTATION_METRICS_PORT=9414 +# Payments methods stack +PAYMENTS_METHODS_DIR=payments_methods +PAYMENTS_METHODS_COMPOSE_PROJECT=sendico-payments-methods +PAYMENTS_METHODS_SERVICE_NAME=sendico_payments_methods +PAYMENTS_METHODS_GRPC_PORT=50066 +PAYMENTS_METHODS_METRICS_PORT=9416 + # Payments orchestrator Mongo settings PAYMENTS_MONGO_HOST=sendico_db1 PAYMENTS_MONGO_PORT=27017 diff --git a/ci/prod/compose/bff.yml b/ci/prod/compose/bff.yml index 39cb5bd4..ff30eefd 100644 --- a/ci/prod/compose/bff.yml +++ b/ci/prod/compose/bff.yml @@ -33,6 +33,7 @@ services: LEDGER_ADDRESS: ${LEDGER_SERVICE_NAME}:${LEDGER_GRPC_PORT} PAYMENTS_ADDRESS: ${PAYMENTS_SERVICE_NAME}:${PAYMENTS_GRPC_PORT} PAYMENTS_QUOTE_ADDRESS: ${PAYMENTS_QUOTATION_SERVICE_NAME}:${PAYMENTS_QUOTATION_GRPC_PORT} + PAYMENTS_METHODS_ADDRESS: ${PAYMENTS_METHODS_SERVICE_NAME}:${PAYMENTS_METHODS_GRPC_PORT} MONGO_HOST: ${MONGO_HOST} MONGO_PORT: ${MONGO_PORT} MONGO_DATABASE: ${MONGO_DATABASE} diff --git a/ci/prod/compose/payments_methods.dockerfile b/ci/prod/compose/payments_methods.dockerfile new file mode 100644 index 00000000..07b3008a --- /dev/null +++ b/ci/prod/compose/payments_methods.dockerfile @@ -0,0 +1,40 @@ +# syntax=docker/dockerfile:1.7 + +ARG TARGETOS=linux +ARG TARGETARCH=amd64 + +FROM golang:alpine AS build +ARG APP_VERSION=dev +ARG GIT_REV=unknown +ARG BUILD_BRANCH=unknown +ARG BUILD_DATE=unknown +ARG BUILD_USER=ci +ENV GO111MODULE=on +ENV PATH="/go/bin:${PATH}" +WORKDIR /src +COPY . . +RUN apk add --no-cache bash git build-base protoc protobuf-dev \ + && go install google.golang.org/protobuf/cmd/protoc-gen-go@latest \ + && go install google.golang.org/grpc/cmd/protoc-gen-go-grpc@latest \ + && bash ci/scripts/proto/generate.sh +WORKDIR /src/api/payments/methods +RUN --mount=type=cache,target=/root/.cache/go-build \ + --mount=type=cache,target=/go/pkg/mod \ + CGO_ENABLED=0 GOOS=${TARGETOS} GOARCH=${TARGETARCH} \ + go build -trimpath -ldflags "\ + -s -w \ + -X github.com/tech/sendico/payments/methods/internal/appversion.Version=${APP_VERSION} \ + -X github.com/tech/sendico/payments/methods/internal/appversion.Revision=${GIT_REV} \ + -X github.com/tech/sendico/payments/methods/internal/appversion.Branch=${BUILD_BRANCH} \ + -X github.com/tech/sendico/payments/methods/internal/appversion.BuildUser=${BUILD_USER} \ + -X github.com/tech/sendico/payments/methods/internal/appversion.BuildDate=${BUILD_DATE}" \ + -o /out/payments-methods . + +FROM alpine:latest AS runtime +RUN apk add --no-cache ca-certificates tzdata wget +WORKDIR /app +COPY api/payments/methods/config.yml /app/config.yml +COPY --from=build /out/payments-methods /app/payments-methods +EXPOSE 50066 9416 +ENTRYPOINT ["/app/payments-methods"] +CMD ["--config.file", "/app/config.yml"] diff --git a/ci/prod/compose/payments_methods.yml b/ci/prod/compose/payments_methods.yml new file mode 100644 index 00000000..4c24fb89 --- /dev/null +++ b/ci/prod/compose/payments_methods.yml @@ -0,0 +1,53 @@ +# Compose v2 - Payments Methods + +x-common-env: &common-env + env_file: + - ../env/.env.runtime + - ../env/.env.version + +networks: + sendico-net: + external: true + name: sendico-net + +services: + sendico_payments_methods: + <<: *common-env + container_name: sendico-payments-methods + restart: unless-stopped + image: ${REGISTRY_URL}/payments/methods:${APP_V} + pull_policy: always + environment: + PAYMENTS_MONGO_HOST: ${PAYMENTS_MONGO_HOST} + PAYMENTS_MONGO_PORT: ${PAYMENTS_MONGO_PORT} + PAYMENTS_MONGO_DATABASE: ${PAYMENTS_MONGO_DATABASE} + PAYMENTS_MONGO_USER: ${PAYMENTS_MONGO_USER} + PAYMENTS_MONGO_PASSWORD: ${PAYMENTS_MONGO_PASSWORD} + PAYMENTS_MONGO_AUTH_SOURCE: ${PAYMENTS_MONGO_AUTH_SOURCE} + PAYMENTS_MONGO_REPLICA_SET: ${PAYMENTS_MONGO_REPLICA_SET} + MONGO_HOST: ${MONGO_HOST} + MONGO_PORT: ${MONGO_PORT} + MONGO_DATABASE: ${MONGO_DATABASE} + MONGO_USER: ${PAYMENTS_MONGO_USER} + MONGO_PASSWORD: ${PAYMENTS_MONGO_PASSWORD} + MONGO_AUTH_SOURCE: ${MONGO_AUTH_SOURCE} + MONGO_REPLICA_SET: ${MONGO_REPLICA_SET} + NATS_URL: ${NATS_URL} + NATS_HOST: ${NATS_HOST} + NATS_PORT: ${NATS_PORT} + NATS_USER: ${NATS_USER} + NATS_PASSWORD: ${NATS_PASSWORD} + PAYMENTS_METHODS_GRPC_PORT: ${PAYMENTS_METHODS_GRPC_PORT} + PAYMENTS_METHODS_METRICS_PORT: ${PAYMENTS_METHODS_METRICS_PORT} + command: ["--config.file", "/app/config.yml"] + ports: + - "0.0.0.0:${PAYMENTS_METHODS_GRPC_PORT}:50066" + - "0.0.0.0:${PAYMENTS_METHODS_METRICS_PORT}:9416" + healthcheck: + test: ["CMD-SHELL","wget -qO- http://localhost:9416/health | grep -q '\"status\":\"ok\"'"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 60s + networks: + - sendico-net diff --git a/ci/prod/scripts/deploy/payments_methods.sh b/ci/prod/scripts/deploy/payments_methods.sh new file mode 100755 index 00000000..01871ca3 --- /dev/null +++ b/ci/prod/scripts/deploy/payments_methods.sh @@ -0,0 +1,147 @@ +#!/usr/bin/env bash +set -euo pipefail +[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && set -x +trap 'echo "[deploy-payments-methods] error at line $LINENO" >&2' ERR + +: "${REMOTE_BASE:?missing REMOTE_BASE}" +: "${SSH_USER:?missing SSH_USER}" +: "${SSH_HOST:?missing SSH_HOST}" +: "${PAYMENTS_METHODS_DIR:?missing PAYMENTS_METHODS_DIR}" +: "${PAYMENTS_METHODS_COMPOSE_PROJECT:?missing PAYMENTS_METHODS_COMPOSE_PROJECT}" +: "${PAYMENTS_METHODS_SERVICE_NAME:?missing PAYMENTS_METHODS_SERVICE_NAME}" + +REMOTE_DIR="${REMOTE_BASE%/}/${PAYMENTS_METHODS_DIR}" +REMOTE_TARGET="${SSH_USER}@${SSH_HOST}" +COMPOSE_FILE="payments_methods.yml" +SERVICE_NAMES="${PAYMENTS_METHODS_SERVICE_NAME}" + +REQUIRED_SECRETS=( + PAYMENTS_MONGO_USER + PAYMENTS_MONGO_PASSWORD + NATS_USER + NATS_PASSWORD + NATS_URL +) + +for var in "${REQUIRED_SECRETS[@]}"; do + if [[ -z "${!var:-}" ]]; then + echo "missing required secret env: ${var}" >&2 + exit 65 + fi +done + +if [[ ! -s .env.version ]]; then + echo ".env.version is missing; run version step first" >&2 + exit 66 +fi + +b64enc() { + printf '%s' "$1" | base64 | tr -d '\n' +} + +PAYMENTS_MONGO_USER_B64="$(b64enc "${PAYMENTS_MONGO_USER}")" +PAYMENTS_MONGO_PASSWORD_B64="$(b64enc "${PAYMENTS_MONGO_PASSWORD}")" +NATS_USER_B64="$(b64enc "${NATS_USER}")" +NATS_PASSWORD_B64="$(b64enc "${NATS_PASSWORD}")" +NATS_URL_B64="$(b64enc "${NATS_URL}")" + +SSH_OPTS=( + -i /root/.ssh/id_rsa + -o StrictHostKeyChecking=no + -o UserKnownHostsFile=/dev/null + -o LogLevel=ERROR + -q +) +if [[ "${DEBUG_DEPLOY:-0}" = "1" ]]; then + SSH_OPTS=("${SSH_OPTS[@]/-q/}" -vv) +fi + +RSYNC_FLAGS=(-az --delete) +[[ "${DEBUG_DEPLOY:-0}" = "1" ]] && RSYNC_FLAGS=(-avz --delete) + +ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" "mkdir -p ${REMOTE_DIR}/{compose,env}" + +rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/compose/ "$REMOTE_TARGET:${REMOTE_DIR}/compose/" +rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" ci/prod/.env.runtime "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.runtime" +rsync "${RSYNC_FLAGS[@]}" -e "ssh ${SSH_OPTS[*]}" .env.version "$REMOTE_TARGET:${REMOTE_DIR}/env/.env.version" + +SERVICES_LINE="${SERVICE_NAMES}" + +ssh "${SSH_OPTS[@]}" "$REMOTE_TARGET" \ + REMOTE_DIR="$REMOTE_DIR" \ + COMPOSE_FILE="$COMPOSE_FILE" \ + COMPOSE_PROJECT="$PAYMENTS_METHODS_COMPOSE_PROJECT" \ + SERVICES_LINE="$SERVICES_LINE" \ + PAYMENTS_MONGO_USER_B64="$PAYMENTS_MONGO_USER_B64" \ + PAYMENTS_MONGO_PASSWORD_B64="$PAYMENTS_MONGO_PASSWORD_B64" \ + NATS_USER_B64="$NATS_USER_B64" \ + NATS_PASSWORD_B64="$NATS_PASSWORD_B64" \ + NATS_URL_B64="$NATS_URL_B64" \ + bash -s <<'EOSSH' +set -euo pipefail +cd "${REMOTE_DIR}/compose" +set -a +. ../env/.env.runtime +load_kv_file() { + local file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + if printf '%s' "$line" | grep -Eq '^[[:alpha:]_][[:alnum:]_]*='; then + local key="${line%%=*}" + local value="${line#*=}" + key="$(printf '%s' "$key" | tr -d '[:space:]')" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + if [[ -n "$key" ]]; then + export "$key=$value" + fi + fi + done <"$file" +} +load_kv_file ../env/.env.version +set +a + +if base64 -d >/dev/null 2>&1 <<<'AA=='; then + BASE64_DECODE_FLAG='-d' +else + BASE64_DECODE_FLAG='--decode' +fi + +decode_b64() { + val="$1" + if [[ -z "$val" ]]; then + printf '' + return + fi + printf '%s' "$val" | base64 "${BASE64_DECODE_FLAG}" +} + +PAYMENTS_MONGO_USER="$(decode_b64 "$PAYMENTS_MONGO_USER_B64")" +PAYMENTS_MONGO_PASSWORD="$(decode_b64 "$PAYMENTS_MONGO_PASSWORD_B64")" +NATS_USER="$(decode_b64 "$NATS_USER_B64")" +NATS_PASSWORD="$(decode_b64 "$NATS_PASSWORD_B64")" +NATS_URL="$(decode_b64 "$NATS_URL_B64")" + +export PAYMENTS_MONGO_USER PAYMENTS_MONGO_PASSWORD NATS_USER NATS_PASSWORD NATS_URL +COMPOSE_PROJECT_NAME="$COMPOSE_PROJECT" +export COMPOSE_PROJECT_NAME +read -r -a SERVICES <<<"${SERVICES_LINE}" + +pull_cmd=(docker compose -f "$COMPOSE_FILE" pull) +up_cmd=(docker compose -f "$COMPOSE_FILE" up -d --remove-orphans) +ps_cmd=(docker compose -f "$COMPOSE_FILE" ps) +if [[ "${#SERVICES[@]}" -gt 0 ]]; then + pull_cmd+=("${SERVICES[@]}") + up_cmd+=("${SERVICES[@]}") + ps_cmd+=("${SERVICES[@]}") +fi + +"${pull_cmd[@]}" +"${up_cmd[@]}" +"${ps_cmd[@]}" + +date -Is > .last_deploy +logger -t "deploy-${COMPOSE_PROJECT_NAME}" "${COMPOSE_PROJECT_NAME} deployed at $(date -Is) in ${REMOTE_DIR}" +EOSSH diff --git a/ci/scripts/payments_methods/build-image.sh b/ci/scripts/payments_methods/build-image.sh new file mode 100755 index 00000000..a2dce25a --- /dev/null +++ b/ci/scripts/payments_methods/build-image.sh @@ -0,0 +1,85 @@ +#!/bin/sh +set -eu + +if ! set -o pipefail 2>/dev/null; then + : +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +sh ci/scripts/common/ensure_env_version.sh + +normalize_env_file() { + file="$1" + tmp="${file}.tmp.$$" + tr -d '\r' <"$file" >"$tmp" + mv "$tmp" "$file" +} + +load_env_file() { + file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + key="${line%%=*}" + value="${line#*=}" + key="$(printf '%s' "$key" | tr -d '[:space:]')" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + export "$key=$value" + done <"$file" +} + +PAYMENTS_METHODS_ENV_NAME="${PAYMENTS_METHODS_ENV:-prod}" +RUNTIME_ENV_FILE="./ci/${PAYMENTS_METHODS_ENV_NAME}/.env.runtime" + +if [ ! -f "${RUNTIME_ENV_FILE}" ]; then + echo "[payments-methods-build] runtime env file not found: ${RUNTIME_ENV_FILE}" >&2 + exit 1 +fi + +normalize_env_file "${RUNTIME_ENV_FILE}" +normalize_env_file ./.env.version + +load_env_file "${RUNTIME_ENV_FILE}" +load_env_file ./.env.version + +REGISTRY_URL="${REGISTRY_URL:?missing REGISTRY_URL}" +APP_V="${APP_V:?missing APP_V}" +PAYMENTS_METHODS_DOCKERFILE="${PAYMENTS_METHODS_DOCKERFILE:?missing PAYMENTS_METHODS_DOCKERFILE}" +PAYMENTS_METHODS_IMAGE_PATH="${PAYMENTS_METHODS_IMAGE_PATH:?missing PAYMENTS_METHODS_IMAGE_PATH}" + +REGISTRY_HOST="${REGISTRY_URL#http://}" +REGISTRY_HOST="${REGISTRY_HOST#https://}" +REGISTRY_USER="$(cat secrets/REGISTRY_USER)" +REGISTRY_PASSWORD="$(cat secrets/REGISTRY_PASSWORD)" +: "${REGISTRY_USER:?missing registry user}" +: "${REGISTRY_PASSWORD:?missing registry password}" + +mkdir -p /kaniko/.docker +AUTH_B64="$(printf '%s:%s' "$REGISTRY_USER" "$REGISTRY_PASSWORD" | base64 | tr -d '\n')" +cat </kaniko/.docker/config.json +{ + "auths": { + "https://${REGISTRY_HOST}": { "auth": "${AUTH_B64}" } + } +} +EOF + +BUILD_CONTEXT="${PAYMENTS_METHODS_BUILD_CONTEXT:-${WOODPECKER_WORKSPACE:-${CI_WORKSPACE:-${PWD:-/workspace}}}}" +if [ ! -d "${BUILD_CONTEXT}" ]; then + BUILD_CONTEXT="/workspace" +fi + +/kaniko/executor \ + --context "${BUILD_CONTEXT}" \ + --dockerfile "${PAYMENTS_METHODS_DOCKERFILE}" \ + --destination "${REGISTRY_URL}/${PAYMENTS_METHODS_IMAGE_PATH}:${APP_V}" \ + --build-arg APP_VERSION="${APP_V}" \ + --build-arg GIT_REV="${GIT_REV}" \ + --build-arg BUILD_BRANCH="${BUILD_BRANCH}" \ + --build-arg BUILD_DATE="${BUILD_DATE}" \ + --build-arg BUILD_USER="${BUILD_USER}" \ + --single-snapshot diff --git a/ci/scripts/payments_methods/deploy.sh b/ci/scripts/payments_methods/deploy.sh new file mode 100755 index 00000000..e81bc4b3 --- /dev/null +++ b/ci/scripts/payments_methods/deploy.sh @@ -0,0 +1,59 @@ +#!/bin/sh +set -eu + +if ! set -o pipefail 2>/dev/null; then + : +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +sh ci/scripts/common/ensure_env_version.sh + +normalize_env_file() { + file="$1" + tmp="${file}.tmp.$$" + tr -d '\r' <"$file" >"$tmp" + mv "$tmp" "$file" +} + +load_env_file() { + file="$1" + while IFS= read -r line || [ -n "$line" ]; do + case "$line" in + ''|\#*) continue ;; + esac + key="${line%%=*}" + value="${line#*=}" + key="$(printf '%s' "$key" | tr -d '[:space:]')" + value="${value#"${value%%[![:space:]]*}"}" + value="${value%"${value##*[![:space:]]}"}" + export "$key=$value" + done <"$file" +} + +. ci/scripts/common/nats_env.sh + +PAYMENTS_METHODS_ENV_NAME="${PAYMENTS_METHODS_ENV:-prod}" +RUNTIME_ENV_FILE="./ci/${PAYMENTS_METHODS_ENV_NAME}/.env.runtime" + +if [ ! -f "${RUNTIME_ENV_FILE}" ]; then + echo "[payments-methods-deploy] runtime env file not found: ${RUNTIME_ENV_FILE}" >&2 + exit 1 +fi + +normalize_env_file "${RUNTIME_ENV_FILE}" +normalize_env_file ./.env.version + +load_env_file "${RUNTIME_ENV_FILE}" +load_env_file ./.env.version + +PAYMENTS_METHODS_MONGO_SECRET_PATH="${PAYMENTS_METHODS_MONGO_SECRET_PATH:?missing PAYMENTS_METHODS_MONGO_SECRET_PATH}" + +export PAYMENTS_MONGO_USER="$(./ci/vlt kv_get kv "${PAYMENTS_METHODS_MONGO_SECRET_PATH}" user)" +export PAYMENTS_MONGO_PASSWORD="$(./ci/vlt kv_get kv "${PAYMENTS_METHODS_MONGO_SECRET_PATH}" password)" + +load_nats_env + +bash ci/prod/scripts/bootstrap/network.sh +bash ci/prod/scripts/deploy/payments_methods.sh diff --git a/ci/scripts/proto/generate.sh b/ci/scripts/proto/generate.sh index c6ff673b..3c7aa9b4 100755 --- a/ci/scripts/proto/generate.sh +++ b/ci/scripts/proto/generate.sh @@ -140,6 +140,12 @@ if [ -f "${PROTO_DIR}/payments/quotation/v1/quotation.proto" ]; then generate_go_with_grpc "${PROTO_DIR}/payments/quotation/v1/quotation.proto" fi +if [ -f "${PROTO_DIR}/payments/methods/v1/methods.proto" ]; then + info "Compiling payments methods protos" + clean_pb_files "./pkg/proto/payments/methods" + generate_go_with_grpc "${PROTO_DIR}/payments/methods/v1/methods.proto" +fi + if [ -f "${PROTO_DIR}/billing/fees/v1/fees.proto" ]; then info "Compiling billing fees protos" clean_pb_files "./pkg/proto/billing/fees" diff --git a/docker-compose.dev.yml b/docker-compose.dev.yml index ee7e82f1..95943698 100644 --- a/docker-compose.dev.yml +++ b/docker-compose.dev.yml @@ -518,6 +518,53 @@ services: ORACLE_ADDRESS: dev-fx-oracle:50051 CHAIN_GATEWAY_ADDRESS: dev-chain-gateway:50053 + # -------------------------------------------------------------------------- + # Payments Methods Service + # -------------------------------------------------------------------------- + dev-payments-methods: + <<: *common-env + build: + context: . + dockerfile: ci/dev/payments-methods.dockerfile + image: sendico-dev/payments-methods:latest + container_name: dev-payments-methods + restart: unless-stopped + depends_on: + dev-mongo-init: { condition: service_completed_successfully } + dev-nats: { condition: service_started } + dev-discovery: { condition: service_started } + volumes: + - ./api/payments/methods:/src/api/payments/methods + - ./api/payments/storage:/src/api/payments/storage + - ./api/payments/methods/config.dev.yml:/app/config.yml:ro + ports: + - "50066:50066" + - "9416:9416" + networks: + - sendico-dev + environment: + PAYMENTS_MONGO_HOST: dev-mongo-1 + PAYMENTS_MONGO_PORT: 27017 + PAYMENTS_MONGO_DATABASE: payments_orchestrator + PAYMENTS_MONGO_USER: ${MONGO_USER} + PAYMENTS_MONGO_PASSWORD: ${MONGO_PASSWORD} + PAYMENTS_MONGO_AUTH_SOURCE: admin + PAYMENTS_MONGO_REPLICA_SET: dev-rs + MONGO_HOST: dev-mongo-1 + MONGO_PORT: 27017 + MONGO_DATABASE: sendico + MONGO_USER: ${MONGO_USER} + MONGO_PASSWORD: ${MONGO_PASSWORD} + MONGO_AUTH_SOURCE: admin + MONGO_REPLICA_SET: dev-rs + NATS_HOST: dev-nats + NATS_PORT: 4222 + NATS_USER: ${NATS_USER} + NATS_PASSWORD: ${NATS_PASSWORD} + NATS_URL: nats://${NATS_USER}:${NATS_PASSWORD}@dev-nats:4222 + PAYMENTS_METHODS_GRPC_PORT: 50066 + PAYMENTS_METHODS_METRICS_PORT: 9416 + # -------------------------------------------------------------------------- # Chain Gateway Vault Agent (sidecar for AppRole authentication) # -------------------------------------------------------------------------- @@ -809,6 +856,7 @@ services: dev-ledger: { condition: service_started } dev-payments-orchestrator: { condition: service_started } dev-payments-quotation: { condition: service_started } + dev-payments-methods: { condition: service_started } dev-chain-gateway: { condition: service_started } volumes: - ./api/server:/src/api/server @@ -839,6 +887,7 @@ services: LEDGER_ADDRESS: dev-ledger:50052 PAYMENTS_ADDRESS: dev-payments-orchestrator:50062 PAYMENTS_QUOTE_ADDRESS: dev-payments-quotation:50064 + PAYMENTS_METHODS_ADDRESS: dev-payments-methods:50066 TRON_GATEWAY_ADDRESS: dev-tron-gateway:50070 BFF_HTTP_PORT: 8080 API_PROTOCOL: http -- 2.49.1 From b5db65ef78da4a353173fad769f46f7471b69402 Mon Sep 17 00:00:00 2001 From: Arseni Date: Fri, 13 Feb 2026 01:03:47 +0300 Subject: [PATCH 12/21] fix for resend, cooldown and a few small fixes --- .../pshared/lib/models/auth/probe_result.dart | 5 + frontend/pshared/lib/provider/account.dart | 29 ++++ .../lib/provider/email_verification.dart | 9 ++ frontend/pweb/lib/app/router/router.dart | 5 +- .../lib/controllers/signup/confirmation.dart | 91 ++++++++++++ .../controllers/signup/confirmation_card.dart | 132 ++++++++++++++++++ frontend/pweb/lib/l10n/en.arb | 2 + frontend/pweb/lib/l10n/ru.arb | 2 + .../pweb/lib/models/resend/action_result.dart | 6 + .../pweb/lib/models/resend/avaliability.dart | 6 + frontend/pweb/lib/pages/2fa/resend.dart | 40 ++---- .../lib/pages/signup/confirmation/args.dart | 3 +- .../pages/signup/confirmation/card/card.dart | 8 +- .../signup/confirmation/card/content.dart | 10 +- .../confirmation/card/resend_button.dart | 36 ----- .../pages/signup/confirmation/card/state.dart | 131 ++++++++--------- .../lib/pages/signup/confirmation/page.dart | 126 ++++++++++------- .../pweb/lib/pages/signup/form/state.dart | 1 + .../pweb/lib/pages/verification/content.dart | 26 ++-- .../lib/pages/verification/controller.dart | 1 + frontend/pweb/lib/providers/two_factor.dart | 48 +++++-- frontend/pweb/lib/utils/cooldown_format.dart | 8 ++ frontend/pweb/lib/utils/error/handler.dart | 4 + frontend/pweb/lib/widgets/resend_link.dart | 48 +++++++ 24 files changed, 550 insertions(+), 227 deletions(-) create mode 100644 frontend/pshared/lib/models/auth/probe_result.dart create mode 100644 frontend/pweb/lib/controllers/signup/confirmation.dart create mode 100644 frontend/pweb/lib/controllers/signup/confirmation_card.dart create mode 100644 frontend/pweb/lib/models/resend/action_result.dart create mode 100644 frontend/pweb/lib/models/resend/avaliability.dart delete mode 100644 frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart create mode 100644 frontend/pweb/lib/utils/cooldown_format.dart create mode 100644 frontend/pweb/lib/widgets/resend_link.dart diff --git a/frontend/pshared/lib/models/auth/probe_result.dart b/frontend/pshared/lib/models/auth/probe_result.dart new file mode 100644 index 00000000..bfa88ca5 --- /dev/null +++ b/frontend/pshared/lib/models/auth/probe_result.dart @@ -0,0 +1,5 @@ +enum AuthProbeResult { + authorized, + notVerified, + error, +} diff --git a/frontend/pshared/lib/provider/account.dart b/frontend/pshared/lib/provider/account.dart index e2c95853..6c3bfcb5 100644 --- a/frontend/pshared/lib/provider/account.dart +++ b/frontend/pshared/lib/provider/account.dart @@ -7,10 +7,12 @@ import 'package:share_plus/share_plus.dart'; import 'package:pshared/api/errors/unauthorized.dart'; import 'package:pshared/api/requests/signup.dart'; import 'package:pshared/api/requests/login_data.dart'; +import 'package:pshared/api/responses/error/server.dart'; import 'package:pshared/api/responses/verification/response.dart'; import 'package:pshared/config/constants.dart'; import 'package:pshared/models/account/account.dart'; import 'package:pshared/models/auth/login_outcome.dart'; +import 'package:pshared/models/auth/probe_result.dart'; import 'package:pshared/models/auth/pending_login.dart'; import 'package:pshared/models/auth/state.dart'; import 'package:pshared/models/describable.dart'; @@ -309,4 +311,31 @@ class AccountProvider extends ChangeNotifier { } await restore(); } + + Future probeAuthorization({ + required String email, + required String password, + required String locale, + }) async { + try { + final outcome = await AccountService.login(LoginData.build( + login: email, + password: password, + locale: locale, + )); + if (outcome.isCompleted) { + await AuthorizationService.logout(); + return AuthProbeResult.authorized; + } + if (outcome.isPending) { + return AuthProbeResult.authorized; + } + return AuthProbeResult.error; + } catch (e) { + if (e is ErrorResponse && e.error == 'account_not_verified') { + return AuthProbeResult.notVerified; + } + return AuthProbeResult.error; + } + } } diff --git a/frontend/pshared/lib/provider/email_verification.dart b/frontend/pshared/lib/provider/email_verification.dart index 9317c458..dc3cc943 100644 --- a/frontend/pshared/lib/provider/email_verification.dart +++ b/frontend/pshared/lib/provider/email_verification.dart @@ -5,6 +5,7 @@ import 'package:pshared/provider/resource.dart'; import 'package:pshared/service/account.dart'; import 'package:pshared/utils/exception.dart'; + class EmailVerificationProvider extends ChangeNotifier { Resource _resource = Resource(data: null, isLoading: false); String? _token; @@ -16,6 +17,14 @@ class EmailVerificationProvider extends ChangeNotifier { ErrorResponse? get errorResponse => _resource.error is ErrorResponse ? _resource.error as ErrorResponse : null; int? get errorCode => errorResponse?.code; + bool get isTokenAlreadyUsed { + final response = errorResponse; + if (response == null) return false; + if (response.code != 409 || response.error != 'data_conflict') { + return false; + } + return response.details.contains('verification token has already been used'); + } bool get canResendVerification => errorCode == 400 || errorCode == 410 || errorCode == 500; diff --git a/frontend/pweb/lib/app/router/router.dart b/frontend/pweb/lib/app/router/router.dart index cc961aab..78eb3165 100644 --- a/frontend/pweb/lib/app/router/router.dart +++ b/frontend/pweb/lib/app/router/router.dart @@ -45,6 +45,9 @@ GoRouter createRouter() => GoRouter( email: state.extra is SignupConfirmationArgs ? (state.extra as SignupConfirmationArgs).email : null, + password: state.extra is SignupConfirmationArgs + ? (state.extra as SignupConfirmationArgs).password + : null, ), ), GoRoute( @@ -57,4 +60,4 @@ GoRouter createRouter() => GoRouter( payoutShellRoute(), ], errorBuilder: (_, _) => const NotFoundPage(), -); \ No newline at end of file +); diff --git a/frontend/pweb/lib/controllers/signup/confirmation.dart b/frontend/pweb/lib/controllers/signup/confirmation.dart new file mode 100644 index 00000000..6c25c82a --- /dev/null +++ b/frontend/pweb/lib/controllers/signup/confirmation.dart @@ -0,0 +1,91 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'package:pshared/models/auth/probe_result.dart'; +import 'package:pshared/provider/account.dart'; + + +class SignupConfirmationController extends ChangeNotifier { + SignupConfirmationController({ + required AccountProvider accountProvider, + Duration pollInterval = const Duration(seconds: 10), + }) : _accountProvider = accountProvider, + _pollInterval = pollInterval; + + final AccountProvider _accountProvider; + final Duration _pollInterval; + + Timer? _pollTimer; + bool _isChecking = false; + bool _isAuthorized = false; + + String? _email; + String? _password; + String? _locale; + + bool get isAuthorized => _isAuthorized; + bool get isChecking => _isChecking; + + void startPolling({ + required String email, + required String password, + required String locale, + }) { + final trimmedEmail = email.trim(); + final trimmedPassword = password.trim(); + final trimmedLocale = locale.trim(); + if (trimmedEmail.isEmpty || trimmedPassword.isEmpty || trimmedLocale.isEmpty) { + return; + } + + _email = trimmedEmail; + _password = trimmedPassword; + _locale = trimmedLocale; + + _pollTimer?.cancel(); + _pollTimer = Timer.periodic(_pollInterval, (_) => _probeAuthorization()); + _probeAuthorization(); + } + + void stopPolling() { + _pollTimer?.cancel(); + _pollTimer = null; + } + + @override + void dispose() { + _pollTimer?.cancel(); + super.dispose(); + } + + Future _probeAuthorization() async { + if (_isChecking || _isAuthorized) return; + final email = _email; + final password = _password; + final locale = _locale; + if (email == null || password == null || locale == null) return; + + _setChecking(true); + try { + final result = await _accountProvider.probeAuthorization( + email: email, + password: password, + locale: locale, + ); + if (result == AuthProbeResult.authorized) { + _isAuthorized = true; + stopPolling(); + notifyListeners(); + } + } finally { + _setChecking(false); + } + } + + void _setChecking(bool value) { + if (_isChecking == value) return; + _isChecking = value; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/controllers/signup/confirmation_card.dart b/frontend/pweb/lib/controllers/signup/confirmation_card.dart new file mode 100644 index 00000000..1b74648b --- /dev/null +++ b/frontend/pweb/lib/controllers/signup/confirmation_card.dart @@ -0,0 +1,132 @@ +import 'dart:async'; + +import 'package:flutter/foundation.dart'; + +import 'package:pshared/provider/account.dart'; + +import 'package:pweb/models/flow_status.dart'; +import 'package:pweb/models/resend/action_result.dart'; +import 'package:pweb/models/resend/avaliability.dart'; + + +class SignupConfirmationCardController extends ChangeNotifier { + SignupConfirmationCardController({ + required AccountProvider accountProvider, + Duration defaultCooldown = const Duration(seconds: 60), + }) : _accountProvider = accountProvider, + _defaultCooldown = defaultCooldown; + + final AccountProvider _accountProvider; + final Duration _defaultCooldown; + + Timer? _cooldownTimer; + DateTime? _cooldownUntil; + int _cooldownRemainingSeconds = 0; + FlowStatus _resendState = FlowStatus.idle; + String? _email; + + int get cooldownRemainingSeconds => _cooldownRemainingSeconds; + ResendAvailability get resendAvailability { + final email = _email; + if (email == null || email.isEmpty) { + return ResendAvailability.missingEmail; + } + if (_resendState == FlowStatus.submitting) { + return ResendAvailability.resending; + } + if (_cooldownRemainingSeconds > 0) { + return ResendAvailability.cooldown; + } + return ResendAvailability.available; + } + + void updateEmail(String? email) { + final trimmed = email?.trim(); + if (_email == trimmed) return; + _email = trimmed; + notifyListeners(); + } + + void initialize({String? email}) { + updateEmail(email); + startDefaultCooldown(); + } + + void startDefaultCooldown() { + _startCooldown(_defaultCooldown); + } + + Future resendVerificationEmail() async { + switch (resendAvailability) { + case ResendAvailability.missingEmail: + return ResendActionResult.missingEmail; + case ResendAvailability.cooldown: + return ResendActionResult.cooldown; + case ResendAvailability.resending: + return ResendActionResult.inProgress; + case ResendAvailability.available: + break; + } + + _setResendState(FlowStatus.submitting); + try { + final email = _email; + if (email == null || email.isEmpty) { + _setResendState(FlowStatus.idle); + return ResendActionResult.missingEmail; + } + await _accountProvider.resendVerificationEmail(email); + _startCooldown(_defaultCooldown); + return ResendActionResult.sent; + } finally { + _setResendState(FlowStatus.idle); + } + } + + @override + void dispose() { + _cooldownTimer?.cancel(); + super.dispose(); + } + + void _startCooldown(Duration duration) { + _cooldownTimer?.cancel(); + _cooldownUntil = DateTime.now().add(duration); + _syncRemaining(); + + if (_cooldownRemainingSeconds <= 0) { + _cooldownUntil = null; + notifyListeners(); + return; + } + + _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { + _syncRemaining(); + if (_cooldownRemainingSeconds <= 0) { + timer.cancel(); + _cooldownUntil = null; + notifyListeners(); + } + }); + } + + void _syncRemaining() { + final remaining = _cooldownRemaining(); + if (remaining == _cooldownRemainingSeconds) return; + _cooldownRemainingSeconds = remaining; + notifyListeners(); + } + + int _cooldownRemaining() { + final until = _cooldownUntil; + if (until == null) return 0; + final remaining = until.difference(DateTime.now()).inSeconds; + return remaining < 0 ? 0 : remaining; + } + + void _setResendState(FlowStatus state) { + if (_resendState == state) return; + _resendState = state; + notifyListeners(); + } +} diff --git a/frontend/pweb/lib/l10n/en.arb b/frontend/pweb/lib/l10n/en.arb index 60811736..d8715c48 100644 --- a/frontend/pweb/lib/l10n/en.arb +++ b/frontend/pweb/lib/l10n/en.arb @@ -660,6 +660,8 @@ "verificationFailed": "Verification Failed", "verificationStatusUnknown": "We couldn't determine the status of your verification. Please try again later.", "verificationStatusErrorUnknown": "Unexpected error occurred while verification. Try once again or contact support", + "accountAlreadyVerified": "Your account has already been verified", + "accountAlreadyVerifiedDescription": "You can now log in to access your account.", "accountVerified": "Account Verified!", "accountVerifiedDescription": "Your account has been successfully verified. You can now log in to access your account.", "retryVerification": "Retry Verification", diff --git a/frontend/pweb/lib/l10n/ru.arb b/frontend/pweb/lib/l10n/ru.arb index 492a9693..72c2ae01 100644 --- a/frontend/pweb/lib/l10n/ru.arb +++ b/frontend/pweb/lib/l10n/ru.arb @@ -662,6 +662,8 @@ "verificationFailed": "Ошибка подтверждения", "verificationStatusUnknown": "Не удалось определить статус подтверждения. Попробуйте позже", "verificationStatusErrorUnknown": "Произошла непредвиденная ошибка при подтверждении. Попробуйте еще раз или обратитесь в службу поддержки", + "accountAlreadyVerified": "Ваш аккаунт уже подтвержден", + "accountAlreadyVerifiedDescription": "Теперь вы можете войти, чтобы получить доступ к аккаунту.", "accountVerified": "Аккаунт подтвержден!", "accountVerifiedDescription": "Ваш аккаунт успешно подтвержден. Теперь вы можете войти, чтобы получить доступ к своему аккаунту", "retryVerification": "Повторить подтверждение", diff --git a/frontend/pweb/lib/models/resend/action_result.dart b/frontend/pweb/lib/models/resend/action_result.dart new file mode 100644 index 00000000..47916a97 --- /dev/null +++ b/frontend/pweb/lib/models/resend/action_result.dart @@ -0,0 +1,6 @@ +enum ResendActionResult { + sent, + missingEmail, + cooldown, + inProgress, +} \ No newline at end of file diff --git a/frontend/pweb/lib/models/resend/avaliability.dart b/frontend/pweb/lib/models/resend/avaliability.dart new file mode 100644 index 00000000..465a2f50 --- /dev/null +++ b/frontend/pweb/lib/models/resend/avaliability.dart @@ -0,0 +1,6 @@ +enum ResendAvailability { + available, + cooldown, + resending, + missingEmail, +} \ No newline at end of file diff --git a/frontend/pweb/lib/pages/2fa/resend.dart b/frontend/pweb/lib/pages/2fa/resend.dart index 8cb8de89..11047e89 100644 --- a/frontend/pweb/lib/pages/2fa/resend.dart +++ b/frontend/pweb/lib/pages/2fa/resend.dart @@ -3,6 +3,8 @@ import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pweb/providers/two_factor.dart'; +import 'package:pweb/utils/cooldown_format.dart'; +import 'package:pweb/widgets/resend_link.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -12,46 +14,20 @@ class ResendCodeButton extends StatelessWidget { @override Widget build(BuildContext context) { - final theme = Theme.of(context); final localizations = AppLocalizations.of(context)!; final provider = context.watch(); final isDisabled = provider.isCooldownActive || provider.isResending; final label = provider.isCooldownActive - ? '${localizations.twoFactorResend} (${_formatCooldown(provider.cooldownRemainingSeconds)})' + ? '${localizations.twoFactorResend} (${formatCooldownSeconds(provider.cooldownRemainingSeconds)})' : localizations.twoFactorResend; - return TextButton( - onPressed: isDisabled ? null : () => provider.resendCode(), - style: TextButton.styleFrom( - padding: EdgeInsets.zero, - minimumSize: const Size(0, 0), - tapTargetSize: MaterialTapTargetSize.shrinkWrap, - alignment: Alignment.centerLeft, - foregroundColor: theme.colorScheme.primary, - textStyle: theme.textTheme.bodyMedium?.copyWith( - decoration: TextDecoration.underline, - ), - ), - child: provider.isResending - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: theme.colorScheme.primary, - ), - ) - : Text(label), + return ResendLink( + label: label, + onPressed: provider.resendCode, + isDisabled: isDisabled, + isLoading: provider.isResending, ); } - String _formatCooldown(int seconds) { - final minutes = seconds ~/ 60; - final remainingSeconds = seconds % 60; - if (minutes > 0) { - return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}'; - } - return remainingSeconds.toString(); - } } diff --git a/frontend/pweb/lib/pages/signup/confirmation/args.dart b/frontend/pweb/lib/pages/signup/confirmation/args.dart index 712d1987..9c577d46 100644 --- a/frontend/pweb/lib/pages/signup/confirmation/args.dart +++ b/frontend/pweb/lib/pages/signup/confirmation/args.dart @@ -1,5 +1,6 @@ class SignupConfirmationArgs { final String? email; + final String? password; - const SignupConfirmationArgs({this.email}); + const SignupConfirmationArgs({this.email, this.password}); } diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/card.dart b/frontend/pweb/lib/pages/signup/confirmation/card/card.dart index 83d2bf9f..8a784cdf 100644 --- a/frontend/pweb/lib/pages/signup/confirmation/card/card.dart +++ b/frontend/pweb/lib/pages/signup/confirmation/card/card.dart @@ -1,13 +1,16 @@ -import 'dart:async'; - import 'package:flutter/material.dart'; import 'package:provider/provider.dart'; import 'package:pshared/provider/account.dart'; +import 'package:pweb/models/resend/action_result.dart'; +import 'package:pweb/models/resend/avaliability.dart'; import 'package:pweb/utils/snackbar.dart'; import 'package:pweb/utils/error/snackbar.dart'; +import 'package:pweb/utils/cooldown_format.dart'; +import 'package:pweb/controllers/signup/confirmation_card.dart'; +import 'package:pweb/widgets/resend_link.dart'; import 'package:pweb/widgets/vspacer.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -15,7 +18,6 @@ import 'package:pweb/generated/i18n/app_localizations.dart'; part 'state.dart'; part 'content.dart'; part 'badge.dart'; -part 'resend_button.dart'; class SignupConfirmationCard extends StatefulWidget { diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/content.dart b/frontend/pweb/lib/pages/signup/confirmation/card/content.dart index 552ca61c..5d78ccb3 100644 --- a/frontend/pweb/lib/pages/signup/confirmation/card/content.dart +++ b/frontend/pweb/lib/pages/signup/confirmation/card/content.dart @@ -63,11 +63,11 @@ class _SignupConfirmationContent extends StatelessWidget { runSpacing: 12, alignment: WrapAlignment.center, children: [ - _SignupConfirmationResendButton( - canResend: canResend, - isResending: isResending, - resendLabel: resendLabel, - onResend: onResend, + ResendLink( + label: resendLabel, + onPressed: onResend, + isDisabled: !canResend, + isLoading: isResending, ), ], ), diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart b/frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart deleted file mode 100644 index dbd6b050..00000000 --- a/frontend/pweb/lib/pages/signup/confirmation/card/resend_button.dart +++ /dev/null @@ -1,36 +0,0 @@ -part of 'card.dart'; - - -class _SignupConfirmationResendButton extends StatelessWidget { - final bool canResend; - final bool isResending; - final String resendLabel; - final VoidCallback onResend; - - const _SignupConfirmationResendButton({ - required this.canResend, - required this.isResending, - required this.resendLabel, - required this.onResend, - }); - - @override - Widget build(BuildContext context) { - final theme = Theme.of(context); - - return ElevatedButton.icon( - onPressed: canResend ? onResend : null, - icon: isResending - ? SizedBox( - width: 16, - height: 16, - child: CircularProgressIndicator( - strokeWidth: 2, - color: theme.colorScheme.onPrimary, - ), - ) - : const Icon(Icons.mark_email_read_outlined), - label: Text(resendLabel), - ); - } -} diff --git a/frontend/pweb/lib/pages/signup/confirmation/card/state.dart b/frontend/pweb/lib/pages/signup/confirmation/card/state.dart index 4c8d28ee..12a21739 100644 --- a/frontend/pweb/lib/pages/signup/confirmation/card/state.dart +++ b/frontend/pweb/lib/pages/signup/confirmation/card/state.dart @@ -3,61 +3,45 @@ part of 'card.dart'; class _SignupConfirmationCardState extends State { static const int _defaultCooldownSeconds = 60; - - Timer? _cooldownTimer; - int _cooldownRemainingSeconds = 0; - bool _isResending = false; + late final SignupConfirmationCardController _controller; @override void initState() { super.initState(); - _startCooldown(_defaultCooldownSeconds); + _controller = SignupConfirmationCardController( + accountProvider: context.read(), + defaultCooldown: const Duration(seconds: _defaultCooldownSeconds), + ); + _controller.initialize(email: widget.email); } @override void dispose() { - _cooldownTimer?.cancel(); + _controller.dispose(); super.dispose(); } - bool get _isCooldownActive => _cooldownRemainingSeconds > 0; - - void _startCooldown(int seconds) { - _cooldownTimer?.cancel(); - if (seconds <= 0) { - setState(() => _cooldownRemainingSeconds = 0); - return; + @override + void didUpdateWidget(covariant SignupConfirmationCard oldWidget) { + super.didUpdateWidget(oldWidget); + if (oldWidget.email != widget.email) { + _controller.updateEmail(widget.email); } - setState(() => _cooldownRemainingSeconds = seconds); - _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (!mounted) { - timer.cancel(); - return; - } - if (_cooldownRemainingSeconds <= 1) { - timer.cancel(); - setState(() => _cooldownRemainingSeconds = 0); - return; - } - setState(() => _cooldownRemainingSeconds -= 1); - }); } Future _resendVerificationEmail() async { - final email = widget.email?.trim(); final locs = AppLocalizations.of(context)!; - if (email == null || email.isEmpty) { - notifyUser(context, locs.errorEmailMissing); - return; - } - if (_isResending || _isCooldownActive) return; - - setState(() => _isResending = true); try { - await context.read().resendVerificationEmail(email); + final result = await _controller.resendVerificationEmail(); if (!mounted) return; - notifyUser(context, locs.signupConfirmationResent(email)); - _startCooldown(_defaultCooldownSeconds); + if (result == ResendActionResult.missingEmail) { + notifyUser(context, locs.errorEmailMissing); + return; + } + if (result == ResendActionResult.sent) { + final email = widget.email?.trim() ?? ''; + notifyUser(context, locs.signupConfirmationResent(email)); + } } catch (e) { if (!mounted) return; postNotifyUserOfErrorX( @@ -65,22 +49,9 @@ class _SignupConfirmationCardState extends State { errorSituation: locs.signupConfirmationResendError, exception: e, ); - } finally { - if (mounted) { - setState(() => _isResending = false); - } } } - String _formatCooldown(int seconds) { - final minutes = seconds ~/ 60; - final remainingSeconds = seconds % 60; - if (minutes > 0) { - return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}'; - } - return remainingSeconds.toString(); - } - @override Widget build(BuildContext context) { final theme = Theme.of(context); @@ -89,34 +60,44 @@ class _SignupConfirmationCardState extends State { final description = (email != null && email.isNotEmpty) ? locs.signupConfirmationDescription(email) : locs.signupConfirmationDescriptionNoEmail; - final canResend = !_isResending && !_isCooldownActive && email != null && email.isNotEmpty; - final resendLabel = _isCooldownActive - ? locs.signupConfirmationResendCooldown(_formatCooldown(_cooldownRemainingSeconds)) - : locs.signupConfirmationResend; - final content = _SignupConfirmationContent( - email: email, - description: description, - canResend: canResend, - resendLabel: resendLabel, - isResending: _isResending, - onResend: _resendVerificationEmail, - ); + return AnimatedBuilder( + animation: _controller, + builder: (context, _) { + final availability = _controller.resendAvailability; + final canResend = availability == ResendAvailability.available; + final isResending = availability == ResendAvailability.resending; + final resendLabel = availability == ResendAvailability.cooldown + ? locs.signupConfirmationResendCooldown( + formatCooldownSeconds(_controller.cooldownRemainingSeconds), + ) + : locs.signupConfirmationResend; - if (widget.isEmbedded) return content; + final content = _SignupConfirmationContent( + email: email, + description: description, + canResend: canResend, + resendLabel: resendLabel, + isResending: isResending, + onResend: _resendVerificationEmail, + ); - return Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(20), - side: BorderSide( - color: theme.dividerColor.withValues(alpha: 0.6), - ), - ), - child: Padding( - padding: const EdgeInsets.all(28), - child: content, - ), + if (widget.isEmbedded) return content; + + return Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(20), + side: BorderSide( + color: theme.dividerColor.withValues(alpha: 0.6), + ), + ), + child: Padding( + padding: const EdgeInsets.all(28), + child: content, + ), + ); + }, ); } } diff --git a/frontend/pweb/lib/pages/signup/confirmation/page.dart b/frontend/pweb/lib/pages/signup/confirmation/page.dart index 2d24331c..523cbed4 100644 --- a/frontend/pweb/lib/pages/signup/confirmation/page.dart +++ b/frontend/pweb/lib/pages/signup/confirmation/page.dart @@ -1,76 +1,106 @@ import 'package:flutter/material.dart'; +import 'package:provider/provider.dart'; + +import 'package:pshared/provider/account.dart'; + +import 'package:pweb/app/router/pages.dart'; import 'package:pweb/pages/login/app_bar.dart'; import 'package:pweb/pages/signup/confirmation/card/card.dart'; -import 'package:pweb/pages/signup/confirmation/login_prompt.dart'; +import 'package:pweb/controllers/signup/confirmation.dart'; import 'package:pweb/pages/with_footer.dart'; -import 'package:pweb/widgets/vspacer.dart'; class SignUpConfirmationPage extends StatefulWidget { final String? email; + final String? password; - const SignUpConfirmationPage({super.key, this.email}); + const SignUpConfirmationPage({ + super.key, + this.email, + this.password, + }); @override State createState() => _SignUpConfirmationPageState(); } class _SignUpConfirmationPageState extends State { + late final SignupConfirmationController _controller; + + @override + void initState() { + super.initState(); + _controller = SignupConfirmationController( + accountProvider: context.read(), + )..addListener(_handleAuthorizationStatus); + WidgetsBinding.instance.addPostFrameCallback((_) => _startPolling()); + } + + @override + void dispose() { + _controller.removeListener(_handleAuthorizationStatus); + _controller.dispose(); + super.dispose(); + } + + void _startPolling() { + if (!mounted) return; + final email = widget.email?.trim(); + final password = widget.password; + if (email == null || email.isEmpty || password == null || password.isEmpty) { + return; + } + _controller.startPolling( + email: email, + password: password, + locale: Localizations.localeOf(context).toLanguageTag(), + ); + } + + void _handleAuthorizationStatus() { + if (!_controller.isAuthorized || !mounted) return; + navigateAndReplace(context, Pages.login); + } + @override Widget build(BuildContext context) { final email = widget.email?.trim(); + final width = MediaQuery.of(context).size.width; + final isWide = width >= 980; return PageWithFooter( appBar: const LoginAppBar(), - child: LayoutBuilder( - builder: (context, constraints) { - final isWide = constraints.maxWidth >= 980; - - return ListView( - padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 32), - children: [ - Center( - child: ConstrainedBox( - constraints: BoxConstraints(maxWidth: isWide ? 980 : 720), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - Card( - elevation: 0, - shape: RoundedRectangleBorder( - borderRadius: BorderRadius.circular(22), - side: BorderSide( - color: Theme.of(context).dividerColor.withValues(alpha: 0.6), - ), - ), - child: Padding( - padding: const EdgeInsets.all(28), - child: Column( - crossAxisAlignment: CrossAxisAlignment.center, - children: [ - SignupConfirmationCard( - email: email, - isEmbedded: true, - ), - const VSpacer(multiplier: 2), - Divider( - height: 1, - color: Theme.of(context).dividerColor.withValues(alpha: 0.6), - ), - const VSpacer(multiplier: 2), - const SignupConfirmationLoginPrompt(isEmbedded: true), - ], - ), - ), + child: Padding( + padding: const EdgeInsets.symmetric(horizontal: 20, vertical: 32), + child: Center( + child: ConstrainedBox( + constraints: BoxConstraints(maxWidth: isWide ? 980 : 720), + child: Column( + mainAxisSize: MainAxisSize.min, + crossAxisAlignment: CrossAxisAlignment.center, + children: [ + Card( + elevation: 0, + shape: RoundedRectangleBorder( + borderRadius: BorderRadius.circular(22), + side: BorderSide( + color: Theme.of(context).dividerColor.withValues(alpha: 0.6), + ), + ), + child: Padding( + padding: const EdgeInsets.all(28), + child: + SignupConfirmationCard( + email: email, + isEmbedded: true, ), - ], ), ), - ), - ], - ); - }, + ], + ), + ), + ), ), ); } diff --git a/frontend/pweb/lib/pages/signup/form/state.dart b/frontend/pweb/lib/pages/signup/form/state.dart index d1f7686a..a8d1fb3f 100644 --- a/frontend/pweb/lib/pages/signup/form/state.dart +++ b/frontend/pweb/lib/pages/signup/form/state.dart @@ -95,6 +95,7 @@ class SignUpFormState extends State { Pages.signupConfirm.name, extra: SignupConfirmationArgs( email: controllers.email.text.trim(), + password: controllers.password.text, ), ); }, diff --git a/frontend/pweb/lib/pages/verification/content.dart b/frontend/pweb/lib/pages/verification/content.dart index c3c90221..9a4db490 100644 --- a/frontend/pweb/lib/pages/verification/content.dart +++ b/frontend/pweb/lib/pages/verification/content.dart @@ -12,6 +12,7 @@ import 'package:pweb/pages/verification/controller.dart'; import 'package:pweb/pages/verification/resend_dialog.dart'; import 'package:pweb/utils/snackbar.dart'; import 'package:pweb/utils/error/snackbar.dart'; +import 'package:pweb/widgets/resend_link.dart'; import 'package:pweb/generated/i18n/app_localizations.dart'; @@ -64,6 +65,12 @@ class AccountVerificationContentState Widget content; if (controller.isLoading) { content = const Center(child: CircularProgressIndicator()); + } else if (controller.isAlreadyVerified) { + content = StatusPageSuccess( + successMessage: locs.accountAlreadyVerified, + successDescription: locs.accountAlreadyVerifiedDescription, + action: action, + ); } else if (controller.isSuccess) { content = StatusPageSuccess( successMessage: locs.accountVerified, @@ -78,18 +85,11 @@ class AccountVerificationContentState exception: controller.error ?? Exception(locs.accountVerificationFailed), action: controller.canResend - ? OutlinedButton.icon( - onPressed: controller.isResending - ? null - : _resendVerificationEmail, - icon: controller.isResending - ? const SizedBox( - width: 18, - height: 18, - child: CircularProgressIndicator(strokeWidth: 2), - ) - : const Icon(Icons.mark_email_unread_outlined), - label: Text(locs.signupConfirmationResend), + ? ResendLink( + label: locs.signupConfirmationResend, + onPressed: _resendVerificationEmail, + isDisabled: !controller.canResend || controller.isResending, + isLoading: controller.isResending, ) : null, ); @@ -108,4 +108,4 @@ class AccountVerificationContentState child: content, ); } -} \ No newline at end of file +} diff --git a/frontend/pweb/lib/pages/verification/controller.dart b/frontend/pweb/lib/pages/verification/controller.dart index c6dc3b3d..5d009e06 100644 --- a/frontend/pweb/lib/pages/verification/controller.dart +++ b/frontend/pweb/lib/pages/verification/controller.dart @@ -26,6 +26,7 @@ class AccountVerificationController extends ChangeNotifier { Exception? get error => _verificationProvider.error; bool get canResend => _verificationProvider.canResendVerification; bool get isResending => _resendStatus == FlowStatus.resending; + bool get isAlreadyVerified => _verificationProvider.isTokenAlreadyUsed; void startVerification(String token) { final trimmed = token.trim(); diff --git a/frontend/pweb/lib/providers/two_factor.dart b/frontend/pweb/lib/providers/two_factor.dart index 85cefca2..309fd4c7 100644 --- a/frontend/pweb/lib/providers/two_factor.dart +++ b/frontend/pweb/lib/providers/two_factor.dart @@ -22,6 +22,7 @@ class TwoFactorProvider extends ChangeNotifier { String? _currentPendingToken; Timer? _cooldownTimer; int _cooldownRemainingSeconds = 0; + DateTime? _cooldownUntil; FlowStatus get status => _status; bool get isSubmitting => _status == FlowStatus.submitting; @@ -108,48 +109,69 @@ class TwoFactorProvider extends ChangeNotifier { return; } - final remaining = pending.cooldownRemainingSeconds; - if (remaining <= 0) { + final until = pending.cooldownUntil; + if (until == null) { _stopCooldown(notify: _cooldownRemainingSeconds != 0); return; } - if (_cooldownRemainingSeconds != remaining) { - _startCooldown(remaining); + if (!_isCooldownActive(until) && _cooldownRemainingSeconds != 0) { + _stopCooldown(notify: true); + return; + } + + if (_cooldownUntil == null || _cooldownUntil != until) { + _startCooldownUntil(until); } } void _startCooldown(int seconds) { + final until = DateTime.now().add(Duration(seconds: seconds)); + _startCooldownUntil(until); + } + + void _startCooldownUntil(DateTime until) { _cooldownTimer?.cancel(); - _cooldownRemainingSeconds = seconds; + _cooldownUntil = until; + _cooldownRemainingSeconds = _cooldownRemaining(); if (_cooldownRemainingSeconds <= 0) { _cooldownTimer = null; + _cooldownUntil = null; notifyListeners(); return; } _cooldownTimer = Timer.periodic(const Duration(seconds: 1), (timer) { - if (_cooldownRemainingSeconds <= 1) { - _cooldownRemainingSeconds = 0; - _cooldownTimer?.cancel(); - _cooldownTimer = null; - notifyListeners(); + final remaining = _cooldownRemaining(); + if (remaining <= 0) { + _stopCooldown(notify: true); return; } - - _cooldownRemainingSeconds -= 1; - notifyListeners(); + if (remaining != _cooldownRemainingSeconds) { + _cooldownRemainingSeconds = remaining; + notifyListeners(); + } }); notifyListeners(); } + bool _isCooldownActive(DateTime until) => until.isAfter(DateTime.now()); + + int _cooldownRemaining() { + final until = _cooldownUntil; + if (until == null) return 0; + final remaining = until.difference(DateTime.now()).inSeconds; + return remaining < 0 ? 0 : remaining; + } + void _stopCooldown({bool notify = false}) { _cooldownTimer?.cancel(); _cooldownTimer = null; final hadCooldown = _cooldownRemainingSeconds != 0; _cooldownRemainingSeconds = 0; + _cooldownUntil = null; if (notify && hadCooldown) { notifyListeners(); diff --git a/frontend/pweb/lib/utils/cooldown_format.dart b/frontend/pweb/lib/utils/cooldown_format.dart new file mode 100644 index 00000000..10a5ee1d --- /dev/null +++ b/frontend/pweb/lib/utils/cooldown_format.dart @@ -0,0 +1,8 @@ +String formatCooldownSeconds(int seconds) { + final minutes = seconds ~/ 60; + final remainingSeconds = seconds % 60; + if (minutes > 0) { + return '$minutes:${remainingSeconds.toString().padLeft(2, '0')}'; + } + return remainingSeconds.toString(); +} diff --git a/frontend/pweb/lib/utils/error/handler.dart b/frontend/pweb/lib/utils/error/handler.dart index 333543e9..97e99072 100644 --- a/frontend/pweb/lib/utils/error/handler.dart +++ b/frontend/pweb/lib/utils/error/handler.dart @@ -15,6 +15,7 @@ class ErrorHandler { 'account_not_verified': locs.errorAccountNotVerified, 'unauthorized': locs.errorLoginUnauthorized, 'verification_token_not_found': locs.errorVerificationTokenNotFound, + 'user_already_registered': locs.errorDuplicateEmail, 'internal_error': locs.errorInternalError, 'invalid_target': locs.errorInvalidTarget, 'pending_token_required': locs.errorPendingTokenRequired, @@ -62,6 +63,9 @@ class ErrorHandler { } static String _handleErrorResponseLocs(AppLocalizations locs, ErrorResponse e) { + if (e.source == 'user_already_registered') { + return locs.errorDuplicateEmail; + } final errorMessages = getErrorMessagesLocs(locs); // Return the localized message if we recognize the error key, else use the raw details return errorMessages[e.error] ?? e.details; diff --git a/frontend/pweb/lib/widgets/resend_link.dart b/frontend/pweb/lib/widgets/resend_link.dart new file mode 100644 index 00000000..7b76a738 --- /dev/null +++ b/frontend/pweb/lib/widgets/resend_link.dart @@ -0,0 +1,48 @@ +import 'package:flutter/material.dart'; + + +class ResendLink extends StatelessWidget { + final String label; + final VoidCallback onPressed; + final bool isDisabled; + final bool isLoading; + + const ResendLink({ + super.key, + required this.label, + required this.onPressed, + this.isDisabled = false, + this.isLoading = false, + }); + + @override + Widget build(BuildContext context) { + final theme = Theme.of(context); + final color = theme.colorScheme.primary; + final isButtonDisabled = isDisabled || isLoading; + + return TextButton( + onPressed: isButtonDisabled ? null : onPressed, + style: TextButton.styleFrom( + padding: EdgeInsets.zero, + minimumSize: const Size(0, 0), + tapTargetSize: MaterialTapTargetSize.shrinkWrap, + alignment: Alignment.centerLeft, + foregroundColor: color, + textStyle: theme.textTheme.bodyMedium?.copyWith( + decoration: TextDecoration.underline, + ), + ), + child: isLoading + ? SizedBox( + width: 16, + height: 16, + child: CircularProgressIndicator( + strokeWidth: 2, + color: color, + ), + ) + : Text(label), + ); + } +} -- 2.49.1 From 16c44ec7d38cb91d2dc9a1ecf49b9bed35dea7b4 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 00:23:39 +0100 Subject: [PATCH 13/21] Moved cursor to a separate file --- api/fx/storage/go.sum | 14 ++++++++------ api/payments/storage/go.sum | 14 ++++++++------ api/proto/common/pagination/v2/cursor.proto | 12 ++++++++++++ api/proto/payments/methods/v1/methods.proto | 9 ++------- .../internal/server/paymethodsimp/service.go | 5 +++-- 5 files changed, 33 insertions(+), 21 deletions(-) create mode 100644 api/proto/common/pagination/v2/cursor.proto diff --git a/api/fx/storage/go.sum b/api/fx/storage/go.sum index b007cadd..a719c09c 100644 --- a/api/fx/storage/go.sum +++ b/api/fx/storage/go.sum @@ -16,6 +16,8 @@ github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0l github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE= github.com/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= @@ -120,12 +122,12 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/api/payments/storage/go.sum b/api/payments/storage/go.sum index b007cadd..a719c09c 100644 --- a/api/payments/storage/go.sum +++ b/api/payments/storage/go.sum @@ -16,6 +16,8 @@ github.com/casbin/mongodb-adapter/v4 v4.3.0 h1:yYXky9v1by6vj/0QK7OyHyd/xpz4vzh0l github.com/casbin/mongodb-adapter/v4 v4.3.0/go.mod h1:bOTSYZUjX7I9E0ExEvgq46m3mcDNRII7g8iWjrM1BHE= github.com/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= @@ -120,12 +122,12 @@ go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y= go.opentelemetry.io/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.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8= -go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM= -go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA= -go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI= -go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE= -go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs= +go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48= +go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8= +go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0= +go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs= +go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI= +go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0= diff --git a/api/proto/common/pagination/v2/cursor.proto b/api/proto/common/pagination/v2/cursor.proto new file mode 100644 index 00000000..10d8d41a --- /dev/null +++ b/api/proto/common/pagination/v2/cursor.proto @@ -0,0 +1,12 @@ +syntax = "proto3"; +package common.pagination.v2; +option go_package = "github.com/tech/sendico/pkg/proto/common/pagination/v2;paginationv2"; + + +import "google/protobuf/wrappers.proto"; + +message ViewCursor { + google.protobuf.Int64Value limit = 1; + google.protobuf.Int64Value offset = 2; + google.protobuf.BoolValue is_archived = 3; +} diff --git a/api/proto/payments/methods/v1/methods.proto b/api/proto/payments/methods/v1/methods.proto index 0aa05d68..37def79f 100644 --- a/api/proto/payments/methods/v1/methods.proto +++ b/api/proto/payments/methods/v1/methods.proto @@ -6,12 +6,7 @@ option go_package = "github.com/tech/sendico/pkg/proto/payments/methods/v1;metho import "google/protobuf/wrappers.proto"; -message ViewCursor { - google.protobuf.Int64Value limit = 1; - google.protobuf.Int64Value offset = 2; - google.protobuf.BoolValue is_archived = 3; -} - +import "common/pagination/v2/cursor.proto"; message CreatePaymentMethodRequest { string account_ref = 1; @@ -63,7 +58,7 @@ message ListPaymentMethodsRequest { string account_ref = 1; string organization_ref = 2; string recipient_ref = 3; - ViewCursor cursor = 4; + common.pagination.v2.ViewCursor cursor = 4; } message ListPaymentMethodsResponse { diff --git a/api/server/internal/server/paymethodsimp/service.go b/api/server/internal/server/paymethodsimp/service.go index 538e3d08..c7763ea4 100644 --- a/api/server/internal/server/paymethodsimp/service.go +++ b/api/server/internal/server/paymethodsimp/service.go @@ -16,6 +16,7 @@ import ( "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" + paginationv2 "github.com/tech/sendico/pkg/proto/common/pagination/v2" methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" eapi "github.com/tech/sendico/server/interface/api" "github.com/tech/sendico/server/interface/api/sresponse" @@ -274,12 +275,12 @@ func resolveClientAddress(service string, cfg *eapi.PaymentOrchestratorConfig) ( return "", merrors.InvalidArgument(strings.TrimSpace(service) + " address is not specified") } -func toProtoCursor(cursor *model.ViewCursor) *methodsv1.ViewCursor { +func toProtoCursor(cursor *model.ViewCursor) *paginationv2.ViewCursor { if cursor == nil { return nil } - res := &methodsv1.ViewCursor{} + res := &paginationv2.ViewCursor{} hasAny := false if cursor.Limit != nil { res.Limit = wrapperspb.Int64(*cursor.Limit) -- 2.49.1 From 1b3aa0f9ea5cf04bae9fd9388cea29dc02590031 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 02:33:22 +0100 Subject: [PATCH 14/21] fixed tests and compilation --- .../internal/ingestor/service_test.go | 2 +- .../internal/service/oracle/service_test.go | 10 +-- api/fx/storage/mongo/repository.go | 4 +- api/fx/storage/mongo/store/quotes.go | 2 +- .../mongo/store/testing_helpers_test.go | 8 +++ .../mongo/store/testing_helpers_test.go | 8 +++ .../methods/internal/service/methods/util.go | 4 +- .../payment_plan_executor_test.go | 62 ------------------- 8 files changed, 27 insertions(+), 73 deletions(-) diff --git a/api/fx/ingestor/internal/ingestor/service_test.go b/api/fx/ingestor/internal/ingestor/service_test.go index 7087a467..16f9b2e1 100644 --- a/api/fx/ingestor/internal/ingestor/service_test.go +++ b/api/fx/ingestor/internal/ingestor/service_test.go @@ -200,7 +200,7 @@ type repositoryStub struct { func (r *repositoryStub) Ping(context.Context) error { return nil } func (r *repositoryStub) Rates() storage.RatesStore { return r.rates } -func (r *repositoryStub) Quotes() quotestorage.QuotesStore { return nil } +func (r *repositoryStub) Quotes() storage.QuotesStore { return nil } func (r *repositoryStub) Pairs() storage.PairStore { return nil } func (r *repositoryStub) Currencies() storage.CurrencyStore { return nil } diff --git a/api/fx/oracle/internal/service/oracle/service_test.go b/api/fx/oracle/internal/service/oracle/service_test.go index 7857f608..be573e63 100644 --- a/api/fx/oracle/internal/service/oracle/service_test.go +++ b/api/fx/oracle/internal/service/oracle/service_test.go @@ -19,16 +19,16 @@ import ( type repositoryStub struct { rates storage.RatesStore - quotes quotestorage.QuotesStore + 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() quotestorage.QuotesStore { return r.quotes } -func (r *repositoryStub) Pairs() storage.PairStore { return r.pairs } +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 } diff --git a/api/fx/storage/mongo/repository.go b/api/fx/storage/mongo/repository.go index 5d02793f..fd469fde 100644 --- a/api/fx/storage/mongo/repository.go +++ b/api/fx/storage/mongo/repository.go @@ -21,7 +21,7 @@ type Store struct { txFactory transaction.Factory rates storage.RatesStore - quotes quotestorage.QuotesStore + quotes storage.QuotesStore pairs storage.PairStore currencies storage.CurrencyStore } @@ -92,7 +92,7 @@ func (s *Store) Rates() storage.RatesStore { return s.rates } -func (s *Store) Quotes() quotestorage.QuotesStore { +func (s *Store) Quotes() storage.QuotesStore { return s.quotes } diff --git a/api/fx/storage/mongo/store/quotes.go b/api/fx/storage/mongo/store/quotes.go index a97026f4..22201768 100644 --- a/api/fx/storage/mongo/store/quotes.go +++ b/api/fx/storage/mongo/store/quotes.go @@ -23,7 +23,7 @@ type quotesStore struct { txFactory transaction.Factory } -func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.Factory) (quotestorage.QuotesStore, error) { +func NewQuotes(logger mlogger.Logger, db *mongo.Database, txFactory transaction.Factory) (storage.QuotesStore, error) { repo := repository.CreateMongoRepository(db, model.QuotesCollection) indexes := []*ri.Definition{ { diff --git a/api/fx/storage/mongo/store/testing_helpers_test.go b/api/fx/storage/mongo/store/testing_helpers_test.go index 27fdbda6..6d10d7f7 100644 --- a/api/fx/storage/mongo/store/testing_helpers_test.go +++ b/api/fx/storage/mongo/store/testing_helpers_test.go @@ -22,6 +22,7 @@ type repoStub struct { 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 + upsertFn 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 } @@ -69,6 +70,13 @@ func (r *repoStub) Update(ctx context.Context, obj storable.Storable) error { return nil } +func (r *repoStub) Upsert(ctx context.Context, obj storable.Storable) error { + if r.upsertFn != nil { + return r.upsertFn(ctx, obj) + } + return nil +} + func (r *repoStub) Patch(ctx context.Context, id bson.ObjectID, patch builder.Patch) error { return merrors.NotImplemented("Patch not used") } diff --git a/api/ledger/storage/mongo/store/testing_helpers_test.go b/api/ledger/storage/mongo/store/testing_helpers_test.go index 2ba30408..1b0f9ef8 100644 --- a/api/ledger/storage/mongo/store/testing_helpers_test.go +++ b/api/ledger/storage/mongo/store/testing_helpers_test.go @@ -19,6 +19,7 @@ type repositoryStub struct { 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 + UpsertFunc func(ctx context.Context, object storable.Storable) error DeleteFunc func(ctx context.Context, id bson.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 @@ -64,6 +65,13 @@ func (r *repositoryStub) Update(ctx context.Context, object storable.Storable) e return nil } +func (r *repositoryStub) Upsert(ctx context.Context, object storable.Storable) error { + if r.UpsertFunc != nil { + return r.UpsertFunc(ctx, object) + } + return nil +} + func (r *repositoryStub) Delete(ctx context.Context, id bson.ObjectID) error { if r.DeleteFunc != nil { return r.DeleteFunc(ctx, id) diff --git a/api/payments/methods/internal/service/methods/util.go b/api/payments/methods/internal/service/methods/util.go index a2eaa539..da1d1fac 100644 --- a/api/payments/methods/internal/service/methods/util.go +++ b/api/payments/methods/internal/service/methods/util.go @@ -11,7 +11,7 @@ import ( "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" - methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" + paginationv2 "github.com/tech/sendico/pkg/proto/common/pagination/v2" "go.mongodb.org/mongo-driver/v2/bson" ) @@ -53,7 +53,7 @@ func encodePaymentMethod(pm *model.PaymentMethod) ([]byte, error) { return payload, nil } -func toModelCursor(cursor *methodsv1.ViewCursor) *model.ViewCursor { +func toModelCursor(cursor *paginationv2.ViewCursor) *model.ViewCursor { if cursor == nil { return nil } diff --git a/api/payments/orchestrator/internal/service/orchestrator/payment_plan_executor_test.go b/api/payments/orchestrator/internal/service/orchestrator/payment_plan_executor_test.go index 371919b4..2a45114b 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/payment_plan_executor_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/payment_plan_executor_test.go @@ -2,7 +2,6 @@ package orchestrator import ( "context" - "strings" "testing" mntxclient "github.com/tech/sendico/gateway/mntx/client" @@ -216,64 +215,3 @@ func TestExecutePaymentPlan_SourceBeforeDestination(t *testing.T) { } } - -func TestExecutePaymentPlan_RejectsLegacyLedgerOperations(t *testing.T) { - ctx := context.Background() - - store := newStubPaymentsStore() - repo := &stubRepository{store: store} - - ledgerFake := &ledgerclient.Fake{} - - svc := &Service{ - logger: zap.NewNop(), - storage: repo, - deps: serviceDependencies{ - ledger: ledgerDependency{ - client: ledgerFake, - internal: ledgerFake, - }, - }, - } - - executor := newPaymentExecutor(&svc.deps, svc.logger, svc) - - payment := &model.Payment{ - PaymentRef: "pay-legacy-1", - IdempotencyKey: "pay-legacy-1", - OrganizationBoundBase: mo.OrganizationBoundBase{ - OrganizationRef: bson.NewObjectID(), - }, - Intent: model.PaymentIntent{ - Ref: "ref-legacy-1", - Kind: model.PaymentKindPayout, - Source: model.PaymentEndpoint{ - Type: model.EndpointTypeManagedWallet, - ManagedWallet: &model.ManagedWalletEndpoint{ - ManagedWalletRef: "wallet-src", - }, - }, - Destination: model.PaymentEndpoint{ - Type: model.EndpointTypeCard, - Card: &model.CardEndpoint{MaskedPan: "4111"}, - }, - }, - PaymentPlan: &model.PaymentPlan{ - ID: "pay-legacy-1", - IdempotencyKey: "pay-legacy-1", - Steps: []*model.PaymentStep{ - {StepID: "ledger_block", Rail: model.RailLedger, Action: model.RailOperationBlock, Amount: &paymenttypes.Money{Currency: "USD", Amount: "100"}}, - }, - }, - } - - store.payments[payment.PaymentRef] = payment - - err := executor.executePaymentPlan(ctx, store, payment, &sharedv1.PaymentQuote{}) - if err == nil { - t.Fatal("expected legacy ledger operation error") - } - if !strings.Contains(err.Error(), "unsupported action") { - t.Fatalf("unexpected error: %v", err) - } -} -- 2.49.1 From 91dc2bd844eba6ab51406850305f455266595d24 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 09:49:22 +0100 Subject: [PATCH 15/21] certificates fix --- ci/prod/compose/frontend.yml | 7 +++++++ frontend/pweb/caddy/Caddyfile | 1 + 2 files changed, 8 insertions(+) diff --git a/ci/prod/compose/frontend.yml b/ci/prod/compose/frontend.yml index ef0c0d7a..938c3f01 100644 --- a/ci/prod/compose/frontend.yml +++ b/ci/prod/compose/frontend.yml @@ -5,6 +5,10 @@ x-common-env: &common-env - ../env/.env.runtime - ../env/.env.version +volumes: + caddy_data: {} + caddy_config: {} + networks: sendico-net: external: true @@ -30,6 +34,9 @@ services: ports: - "0.0.0.0:${FRONTEND_HTTP_PORT}:80" - "0.0.0.0:${FRONTEND_HTTPS_PORT}:443" + volumes: + - caddy_data:/data + - caddy_config:/config healthcheck: test: ["CMD", "curl", "-f", "http://localhost:2019/config"] interval: 30s diff --git a/frontend/pweb/caddy/Caddyfile b/frontend/pweb/caddy/Caddyfile index 9ea9bdd5..c1fc8759 100644 --- a/frontend/pweb/caddy/Caddyfile +++ b/frontend/pweb/caddy/Caddyfile @@ -3,6 +3,7 @@ ######################################## { email {$CADDY_ACME_EMAIL} + acme_ca https://acme.zerossl.com/v2/DV90 # debug } -- 2.49.1 From 8704a968d24acf4fd6f4c995b3a4e3812d6a301b Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 12:14:38 +0100 Subject: [PATCH 16/21] autotests --- .woodpecker/bff.yml | 10 +- .woodpecker/billing_documents.yml | 10 +- .woodpecker/billing_fees.yml | 10 +- .woodpecker/discovery.yml | 10 +- .woodpecker/fx_ingestor.yml | 10 +- .woodpecker/fx_oracle.yml | 10 +- .woodpecker/gateway_chain.yml | 10 +- .woodpecker/gateway_mntx.yml | 10 +- .woodpecker/gateway_tgsettle.yml | 10 +- .woodpecker/gateway_tron.yml | 10 +- .woodpecker/ledger.yml | 10 +- .woodpecker/notification.yml | 10 +- .woodpecker/payments_methods.yml | 10 +- .woodpecker/payments_orchestrator.yml | 10 +- .woodpecker/payments_quotation.yml | 10 +- ci/scripts/common/run_backend_tests.sh | 135 +++++++++++++++++++++++++ 16 files changed, 270 insertions(+), 15 deletions(-) create mode 100755 ci/scripts/common/run_backend_tests.sh diff --git a/.woodpecker/bff.yml b/.woodpecker/bff.yml index 63cef64a..0a656f5b 100644 --- a/.woodpecker/bff.yml +++ b/.woodpecker/bff.yml @@ -44,6 +44,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh bff + - name: secrets image: alpine:latest depends_on: [ version ] @@ -64,7 +72,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/bff/build-image.sh diff --git a/.woodpecker/billing_documents.yml b/.woodpecker/billing_documents.yml index 38f67056..993ae421 100644 --- a/.woodpecker/billing_documents.yml +++ b/.woodpecker/billing_documents.yml @@ -40,6 +40,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh billing_documents + - name: secrets image: alpine:latest depends_on: [ version ] @@ -60,7 +68,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/billing_documents/build-image.sh diff --git a/.woodpecker/billing_fees.yml b/.woodpecker/billing_fees.yml index 10e303e5..4665426c 100644 --- a/.woodpecker/billing_fees.yml +++ b/.woodpecker/billing_fees.yml @@ -40,6 +40,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh billing_fees + - name: secrets image: alpine:latest depends_on: [ version ] @@ -60,7 +68,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/billing_fees/build-image.sh diff --git a/.woodpecker/discovery.yml b/.woodpecker/discovery.yml index 9cd58c4f..e50d8ee1 100644 --- a/.woodpecker/discovery.yml +++ b/.woodpecker/discovery.yml @@ -39,6 +39,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh discovery + - name: secrets image: alpine:latest depends_on: [ version ] @@ -59,7 +67,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/discovery/build-image.sh diff --git a/.woodpecker/fx_ingestor.yml b/.woodpecker/fx_ingestor.yml index b063fb6d..f815324e 100644 --- a/.woodpecker/fx_ingestor.yml +++ b/.woodpecker/fx_ingestor.yml @@ -45,6 +45,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh fx_ingestor + - name: secrets image: alpine:latest depends_on: [ version ] @@ -65,7 +73,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/fx/build-image.sh diff --git a/.woodpecker/fx_oracle.yml b/.woodpecker/fx_oracle.yml index 38fb8e63..39e498dd 100644 --- a/.woodpecker/fx_oracle.yml +++ b/.woodpecker/fx_oracle.yml @@ -46,6 +46,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh fx_oracle + - name: secrets image: alpine:latest depends_on: [ version ] @@ -66,7 +74,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/fx/build-image.sh diff --git a/.woodpecker/gateway_chain.yml b/.woodpecker/gateway_chain.yml index d8103392..aa13ae7b 100644 --- a/.woodpecker/gateway_chain.yml +++ b/.woodpecker/gateway_chain.yml @@ -43,6 +43,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh gateway_chain + - name: secrets image: alpine:latest depends_on: [ version ] @@ -63,7 +71,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/chain_gateway/build-image.sh diff --git a/.woodpecker/gateway_mntx.yml b/.woodpecker/gateway_mntx.yml index f5de3e00..bc7ba934 100644 --- a/.woodpecker/gateway_mntx.yml +++ b/.woodpecker/gateway_mntx.yml @@ -42,6 +42,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh gateway_mntx + - name: secrets image: alpine:latest depends_on: [ version ] @@ -62,7 +70,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/mntx/build-image.sh diff --git a/.woodpecker/gateway_tgsettle.yml b/.woodpecker/gateway_tgsettle.yml index ef9c5cd6..63db1e1a 100644 --- a/.woodpecker/gateway_tgsettle.yml +++ b/.woodpecker/gateway_tgsettle.yml @@ -40,6 +40,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh gateway_tgsettle + - name: secrets image: alpine:latest depends_on: [ version ] @@ -60,7 +68,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/tgsettle/build-image.sh diff --git a/.woodpecker/gateway_tron.yml b/.woodpecker/gateway_tron.yml index 958e7a69..576bc07d 100644 --- a/.woodpecker/gateway_tron.yml +++ b/.woodpecker/gateway_tron.yml @@ -43,6 +43,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh gateway_tron + - name: secrets image: alpine:latest depends_on: [ version ] @@ -63,7 +71,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/tron_gateway/build-image.sh diff --git a/.woodpecker/ledger.yml b/.woodpecker/ledger.yml index b0d7fdc6..97b75c67 100644 --- a/.woodpecker/ledger.yml +++ b/.woodpecker/ledger.yml @@ -40,6 +40,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh ledger + - name: secrets image: alpine:latest depends_on: [ version ] @@ -60,7 +68,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/ledger/build-image.sh diff --git a/.woodpecker/notification.yml b/.woodpecker/notification.yml index 761859eb..86567941 100644 --- a/.woodpecker/notification.yml +++ b/.woodpecker/notification.yml @@ -43,6 +43,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh notification + - name: secrets image: alpine:latest depends_on: [ version ] @@ -63,7 +71,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/notification/build-image.sh diff --git a/.woodpecker/payments_methods.yml b/.woodpecker/payments_methods.yml index 7c237649..79bf8309 100644 --- a/.woodpecker/payments_methods.yml +++ b/.woodpecker/payments_methods.yml @@ -40,6 +40,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh payments_methods + - name: secrets image: alpine:latest depends_on: [ version ] @@ -60,7 +68,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/payments_methods/build-image.sh diff --git a/.woodpecker/payments_orchestrator.yml b/.woodpecker/payments_orchestrator.yml index 7558af26..d78489c8 100644 --- a/.woodpecker/payments_orchestrator.yml +++ b/.woodpecker/payments_orchestrator.yml @@ -40,6 +40,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh payments_orchestrator + - name: secrets image: alpine:latest depends_on: [ version ] @@ -60,7 +68,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/payments_orchestrator/build-image.sh diff --git a/.woodpecker/payments_quotation.yml b/.woodpecker/payments_quotation.yml index 0b328ee5..83710779 100644 --- a/.woodpecker/payments_quotation.yml +++ b/.woodpecker/payments_quotation.yml @@ -42,6 +42,14 @@ steps: - export PATH="$(go env GOPATH)/bin:$PATH" - bash ci/scripts/proto/generate.sh + - name: backend-tests + image: golang:alpine + depends_on: [ proto ] + commands: + - set -eu + - apk add --no-cache bash git build-base + - sh ci/scripts/common/run_backend_tests.sh payments_quotation + - name: secrets image: alpine:latest depends_on: [ version ] @@ -62,7 +70,7 @@ steps: - name: build-image image: gcr.io/kaniko-project/executor:debug - depends_on: [ proto, secrets ] + depends_on: [ backend-tests, secrets ] commands: - sh ci/scripts/payments_quotation/build-image.sh diff --git a/ci/scripts/common/run_backend_tests.sh b/ci/scripts/common/run_backend_tests.sh new file mode 100755 index 00000000..ce13c26c --- /dev/null +++ b/ci/scripts/common/run_backend_tests.sh @@ -0,0 +1,135 @@ +#!/usr/bin/env bash +set -eu + +if ! set -o pipefail 2>/dev/null; then + : +fi + +SERVICE="${1:-}" +if [ -z "${SERVICE}" ]; then + echo "usage: $0 " >&2 + exit 2 +fi + +REPO_ROOT="$(cd "$(dirname "$0")/../../.." && pwd)" +cd "${REPO_ROOT}" + +run_go_tests() { + module="$1" + if [ ! -f "${module}/go.mod" ]; then + echo "[backend-tests] missing go.mod for module: ${module}" >&2 + exit 1 + fi + + echo "[backend-tests] running go test ./... in ${module}" + ( + cd "${module}" + go test ./... + ) +} + +case "${SERVICE}" in + bff) + modules=" +api/pkg +api/server +" + ;; + billing_documents) + modules=" +api/pkg +api/billing/documents +" + ;; + billing_fees) + modules=" +api/pkg +api/billing/fees +" + ;; + discovery) + modules=" +api/pkg +api/discovery +" + ;; + fx_ingestor) + modules=" +api/pkg +api/fx/storage +api/fx/ingestor +" + ;; + fx_oracle) + modules=" +api/pkg +api/fx/storage +api/fx/oracle +" + ;; + gateway_chain) + modules=" +api/pkg +api/gateway/chain +" + ;; + gateway_mntx) + modules=" +api/pkg +api/gateway/mntx +" + ;; + gateway_tgsettle) + modules=" +api/pkg +api/gateway/tgsettle +" + ;; + gateway_tron) + modules=" +api/pkg +api/gateway/tron +" + ;; + ledger) + modules=" +api/pkg +api/ledger +" + ;; + notification) + modules=" +api/pkg +api/notification +" + ;; + payments_methods) + modules=" +api/pkg +api/payments/storage +api/payments/methods +" + ;; + payments_orchestrator) + modules=" +api/pkg +api/payments/storage +api/payments/orchestrator +" + ;; + payments_quotation) + modules=" +api/pkg +api/payments/storage +api/payments/quotation +" + ;; + *) + echo "[backend-tests] unknown service key: ${SERVICE}" >&2 + exit 2 + ;; +esac + +for module in ${modules}; do + run_go_tests "${module}" +done -- 2.49.1 From 52c4c046c9045d156387dadcb4032d645dd7aa03 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 15:35:17 +0100 Subject: [PATCH 17/21] neq quotation definition + priced_at field --- api/billing/documents/go.mod | 2 +- api/billing/documents/go.sum | 4 +- api/billing/fees/go.mod | 2 +- api/billing/fees/go.sum | 4 +- api/discovery/go.mod | 2 +- api/discovery/go.sum | 4 +- api/fx/ingestor/go.mod | 2 +- api/fx/ingestor/go.sum | 4 +- api/fx/oracle/client/client.go | 6 + api/fx/oracle/client/client_test.go | 6 + api/fx/oracle/go.mod | 2 +- api/fx/oracle/go.sum | 4 +- .../internal/service/oracle/calculator.go | 5 + .../internal/service/oracle/service_test.go | 9 +- .../internal/service/oracle/transform.go | 13 + api/fx/storage/model/quote.go | 1 + api/fx/storage/mongo/store/quotes.go | 3 + api/fx/storage/mongo/store/quotes_test.go | 3 + api/gateway/chain/go.mod | 4 +- api/gateway/chain/go.sum | 8 +- api/gateway/mntx/go.mod | 2 +- api/gateway/mntx/go.sum | 4 +- api/gateway/tgsettle/go.mod | 2 +- api/gateway/tgsettle/go.sum | 4 +- api/gateway/tron/go.mod | 4 +- api/gateway/tron/go.sum | 8 +- api/ledger/go.mod | 2 +- api/ledger/go.sum | 4 +- api/notification/go.mod | 2 +- api/notification/go.sum | 4 +- api/payments/methods/go.mod | 4 +- api/payments/methods/go.sum | 4 +- .../internal/service/methods/create.go | 6 +- .../methods/internal/service/methods/get.go | 4 +- .../methods/internal/service/methods/list.go | 9 +- .../internal/service/methods/update.go | 6 +- .../methods/internal/service/methods/util.go | 231 +++++++++++++- api/payments/orchestrator/go.mod | 2 +- api/payments/orchestrator/go.sum | 4 +- .../internal/service/orchestrator/convert.go | 10 + .../orchestrator/convert_types_test.go | 10 + .../orchestrator/payment_plan_storage.go | 1 + .../internal/service/plan_builder/helpers.go | 12 + api/payments/quotation/go.mod | 2 +- api/payments/quotation/go.sum | 4 +- .../internal/service/plan/helpers.go | 12 + .../internal/service/quotation/convert.go | 12 + .../internal/service/quotation/helpers.go | 6 + .../service/quotation/payment_plan_factory.go | 1 + api/pkg/go.mod | 2 +- api/pkg/go.sum | 4 +- api/pkg/payments/types/fx.go | 8 + api/proto/billing/fees/v1/fees.proto | 20 +- .../common/archivable/v1/archivable.proto | 11 + api/proto/common/gateway/v1/gateway.proto | 20 +- .../common/organization_bound/v1/obound.proto | 10 + api/proto/common/payment/v1/card.proto | 73 +++++ api/proto/common/payment/v1/custom.proto | 11 + .../common/payment/v1/external_chain.proto | 16 + api/proto/common/payment/v1/ledger.proto | 14 + .../common/payment/v1/managed_wallet.proto | 11 + api/proto/common/payment/v1/rba.proto | 16 + api/proto/common/payment/v1/sepa.proto | 17 + api/proto/common/payment/v1/settlement.proto | 13 + .../common/permission_bound/v1/pbound.proto | 17 + api/proto/common/storable/v1/storable.proto | 14 + api/proto/connector/v1/connector.proto | 8 +- api/proto/gateway/chain/v1/chain.proto | 6 +- api/proto/gateway/mntx/v1/mntx.proto | 2 +- api/proto/ledger/v1/ledger.proto | 4 +- api/proto/oracle/v1/oracle.proto | 9 +- api/proto/payments/endpoint/v1/endpoint.proto | 40 +++ api/proto/payments/methods/v1/methods.proto | 24 +- .../orchestration/v1/orchestration.proto | 10 +- api/proto/payments/payment/v1/payment.proto | 20 ++ .../payments/quotation/v1/quotation.proto | 5 +- .../payments/quotation/v2/interface.proto | 59 ++++ .../payments/quotation/v2/quotation.proto | 37 +++ api/proto/payments/shared/v1/shared.proto | 24 +- api/proto/payments/transfer/v1/transfer.proto | 21 ++ api/server/go.mod | 2 +- api/server/go.sum | 4 +- api/server/interface/api/sresponse/payment.go | 6 + .../internal/server/paymethodsimp/service.go | 291 +++++++++++++++++- ci/scripts/proto/generate.sh | 29 +- 85 files changed, 1180 insertions(+), 162 deletions(-) create mode 100644 api/proto/common/archivable/v1/archivable.proto create mode 100644 api/proto/common/organization_bound/v1/obound.proto create mode 100644 api/proto/common/payment/v1/card.proto create mode 100644 api/proto/common/payment/v1/custom.proto create mode 100644 api/proto/common/payment/v1/external_chain.proto create mode 100644 api/proto/common/payment/v1/ledger.proto create mode 100644 api/proto/common/payment/v1/managed_wallet.proto create mode 100644 api/proto/common/payment/v1/rba.proto create mode 100644 api/proto/common/payment/v1/sepa.proto create mode 100644 api/proto/common/payment/v1/settlement.proto create mode 100644 api/proto/common/permission_bound/v1/pbound.proto create mode 100644 api/proto/common/storable/v1/storable.proto create mode 100644 api/proto/payments/endpoint/v1/endpoint.proto create mode 100644 api/proto/payments/payment/v1/payment.proto create mode 100644 api/proto/payments/quotation/v2/interface.proto create mode 100644 api/proto/payments/quotation/v2/quotation.proto create mode 100644 api/proto/payments/transfer/v1/transfer.proto diff --git a/api/billing/documents/go.mod b/api/billing/documents/go.mod index 23467825..cf89e01d 100644 --- a/api/billing/documents/go.mod +++ b/api/billing/documents/go.mod @@ -15,7 +15,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/billing/documents/go.sum b/api/billing/documents/go.sum index b2fa29f2..cbc4195b 100644 --- a/api/billing/documents/go.sum +++ b/api/billing/documents/go.sum @@ -260,8 +260,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/billing/fees/go.mod b/api/billing/fees/go.mod index 7e99f7ee..7c9329a4 100644 --- a/api/billing/fees/go.mod +++ b/api/billing/fees/go.mod @@ -10,7 +10,7 @@ require ( github.com/tech/sendico/fx/oracle v0.0.0 github.com/tech/sendico/pkg v0.1.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/billing/fees/go.sum b/api/billing/fees/go.sum index 35825f0b..3552c709 100644 --- a/api/billing/fees/go.sum +++ b/api/billing/fees/go.sum @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/discovery/go.mod b/api/discovery/go.mod index c1005c0b..aacafd49 100644 --- a/api/discovery/go.mod +++ b/api/discovery/go.mod @@ -44,6 +44,6 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.0 // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/discovery/go.sum b/api/discovery/go.sum index 35825f0b..3552c709 100644 --- a/api/discovery/go.sum +++ b/api/discovery/go.sum @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/ingestor/go.mod b/api/fx/ingestor/go.mod index a9defa7e..eb28b3f9 100644 --- a/api/fx/ingestor/go.mod +++ b/api/fx/ingestor/go.mod @@ -48,6 +48,6 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.0 // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/fx/ingestor/go.sum b/api/fx/ingestor/go.sum index 35825f0b..3552c709 100644 --- a/api/fx/ingestor/go.sum +++ b/api/fx/ingestor/go.sum @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/oracle/client/client.go b/api/fx/oracle/client/client.go index ca8b6fda..81453ebd 100644 --- a/api/fx/oracle/client/client.go +++ b/api/fx/oracle/client/client.go @@ -68,6 +68,7 @@ type Quote struct { BaseAmount *moneyv1.Money QuoteAmount *moneyv1.Money ExpiresAt time.Time + PricedAt time.Time Provider string RateRef string Firm bool @@ -237,6 +238,10 @@ func fromProtoQuote(quote *oraclev1.Quote) *Quote { if quote == nil { return nil } + pricedAt := time.Time{} + if ts := quote.GetPricedAt(); ts != nil { + pricedAt = ts.AsTime() + } return &Quote{ QuoteRef: quote.GetQuoteRef(), Pair: quote.Pair, @@ -245,6 +250,7 @@ func fromProtoQuote(quote *oraclev1.Quote) *Quote { BaseAmount: quote.BaseAmount, QuoteAmount: quote.QuoteAmount, ExpiresAt: time.UnixMilli(quote.GetExpiresAtUnixMs()), + PricedAt: pricedAt, 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 index 3a2aca1e..06778980 100644 --- a/api/fx/oracle/client/client_test.go +++ b/api/fx/oracle/client/client_test.go @@ -9,6 +9,7 @@ import ( moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" "google.golang.org/grpc" + "google.golang.org/protobuf/types/known/timestamppb" ) type stubOracle struct { @@ -75,6 +76,7 @@ func TestLatestRate(t *testing.T) { func TestGetQuote(t *testing.T) { expiresAt := time.Date(2024, 2, 2, 12, 0, 0, 0, time.UTC) + pricedAt := time.Date(2024, 2, 2, 11, 59, 0, 0, time.UTC) stub := &stubOracle{ quoteResp: &oraclev1.GetQuoteResponse{ Quote: &oraclev1.Quote{ @@ -85,6 +87,7 @@ func TestGetQuote(t *testing.T) { BaseAmount: &moneyv1.Money{Amount: "100.00", Currency: "GBP"}, QuoteAmount: &moneyv1.Money{Amount: "125.00", Currency: "USD"}, ExpiresAtUnixMs: expiresAt.UnixMilli(), + PricedAt: timestamppb.New(pricedAt), Provider: "Test", RateRef: "test-ref", Firm: true, @@ -113,4 +116,7 @@ func TestGetQuote(t *testing.T) { if resp.QuoteRef != "quote-123" || resp.Price != "1.2500" || !resp.ExpiresAt.Equal(expiresAt) { t.Fatalf("unexpected quote response: %+v", resp) } + if !resp.PricedAt.Equal(pricedAt) { + t.Fatalf("expected priced_at %s, got %s", pricedAt, resp.PricedAt) + } } diff --git a/api/fx/oracle/go.mod b/api/fx/oracle/go.mod index b7ea0e59..a79335c1 100644 --- a/api/fx/oracle/go.mod +++ b/api/fx/oracle/go.mod @@ -13,7 +13,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/fx/oracle/go.sum b/api/fx/oracle/go.sum index 35825f0b..3552c709 100644 --- a/api/fx/oracle/go.sum +++ b/api/fx/oracle/go.sum @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/fx/oracle/internal/service/oracle/calculator.go b/api/fx/oracle/internal/service/oracle/calculator.go index cb4790ca..fc55c471 100644 --- a/api/fx/oracle/internal/service/oracle/calculator.go +++ b/api/fx/oracle/internal/service/oracle/calculator.go @@ -130,6 +130,10 @@ func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req * if qc.baseRounded == nil || qc.quoteRounded == nil || qc.priceRounded == nil { return nil, merrors.Internal("oracle: computation not executed") } + pricedAtUnixMs := qc.rate.AsOfUnixMs + if pricedAtUnixMs <= 0 { + pricedAtUnixMs = time.Now().UnixMilli() + } quote := &model.Quote{ QuoteRef: uuid.NewString(), @@ -147,6 +151,7 @@ func (qc *quoteComputation) buildModelQuote(firm bool, expiryMillis int64, req * Amount: formatRat(qc.quoteRounded, qc.quoteScale), }, AmountType: qc.amountType, + PricedAtUnixMs: pricedAtUnixMs, RateRef: qc.rate.RateRef, Provider: qc.provider, PreferredProvider: req.GetPreferredProvider(), diff --git a/api/fx/oracle/internal/service/oracle/service_test.go b/api/fx/oracle/internal/service/oracle/service_test.go index be573e63..a8c5f2de 100644 --- a/api/fx/oracle/internal/service/oracle/service_test.go +++ b/api/fx/oracle/internal/service/oracle/service_test.go @@ -111,6 +111,7 @@ func (currencyStoreStub) List(ctx context.Context, codes ...string) ([]*model.Cu func (currencyStoreStub) Upsert(ctx context.Context, currency *model.Currency) error { return nil } func TestServiceGetQuoteFirm(t *testing.T) { + pricedAt := time.Date(2024, 1, 2, 3, 4, 5, 0, time.UTC) repo := &repositoryStub{} repo.pairs = &pairStoreStub{ getFn: func(ctx context.Context, pair model.CurrencyPair) (*model.Pair, error) { @@ -129,7 +130,7 @@ func TestServiceGetQuoteFirm(t *testing.T) { Ask: "1.10", Bid: "1.08", RateRef: "rate#1", - AsOfUnixMs: time.Now().UnixMilli(), + AsOfUnixMs: pricedAt.UnixMilli(), }, nil }, } @@ -169,9 +170,15 @@ func TestServiceGetQuoteFirm(t *testing.T) { if resp.GetQuote().GetQuoteAmount().GetAmount() != "110.00" { t.Fatalf("unexpected quote amount: %s", resp.GetQuote().GetQuoteAmount().GetAmount()) } + if got := resp.GetQuote().GetPricedAt(); got == nil || !got.AsTime().Equal(pricedAt) { + t.Fatalf("expected priced_at %s, got %v", pricedAt, got) + } if savedQuote.QuoteRef == "" { t.Fatalf("expected quote persisted") } + if savedQuote.PricedAtUnixMs != pricedAt.UnixMilli() { + t.Fatalf("expected stored pricedAtUnixMs %d, got %d", pricedAt.UnixMilli(), savedQuote.PricedAtUnixMs) + } } func TestServiceGetQuoteRateNotFound(t *testing.T) { diff --git a/api/fx/oracle/internal/service/oracle/transform.go b/api/fx/oracle/internal/service/oracle/transform.go index b96f8aae..d68ef8d4 100644 --- a/api/fx/oracle/internal/service/oracle/transform.go +++ b/api/fx/oracle/internal/service/oracle/transform.go @@ -2,12 +2,14 @@ package oracle import ( "strings" + "time" "github.com/tech/sendico/fx/storage/model" paymenttypes "github.com/tech/sendico/pkg/payments/types" 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/protobuf/types/known/timestamppb" ) func buildResponseMeta(meta *oraclev1.RequestMeta) *oraclev1.ResponseMeta { @@ -36,6 +38,7 @@ func quoteModelToProto(q *model.Quote) *oraclev1.Quote { BaseAmount: moneyModelToProto(&q.BaseAmount), QuoteAmount: moneyModelToProto(&q.QuoteAmount), ExpiresAtUnixMs: q.ExpiresAtUnixMs, + PricedAt: timestampFromUnixMillis(q.PricedAtUnixMs, q.CreatedAt), Provider: q.Provider, RateRef: q.RateRef, Firm: q.Firm, @@ -117,3 +120,13 @@ func decimalStringToProto(value string) *moneyv1.Decimal { } return &moneyv1.Decimal{Value: value} } + +func timestampFromUnixMillis(ms int64, fallback time.Time) *timestamppb.Timestamp { + if ms > 0 { + return timestamppb.New(time.UnixMilli(ms).UTC()) + } + if !fallback.IsZero() { + return timestamppb.New(fallback.UTC()) + } + return nil +} diff --git a/api/fx/storage/model/quote.go b/api/fx/storage/model/quote.go index 7b15580f..1274e132 100644 --- a/api/fx/storage/model/quote.go +++ b/api/fx/storage/model/quote.go @@ -21,6 +21,7 @@ type Quote struct { QuoteAmount paymenttypes.Money `bson:"quoteAmount" json:"quoteAmount"` AmountType QuoteAmountType `bson:"amountType" json:"amountType"` ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs" json:"expiresAtUnixMs"` + PricedAtUnixMs int64 `bson:"pricedAtUnixMs,omitempty" json:"pricedAtUnixMs,omitempty"` ExpiresAt *time.Time `bson:"expiresAt,omitempty" json:"expiresAt,omitempty"` RateRef string `bson:"rateRef" json:"rateRef"` Provider string `bson:"provider" json:"provider"` diff --git a/api/fx/storage/mongo/store/quotes.go b/api/fx/storage/mongo/store/quotes.go index 22201768..4fca596f 100644 --- a/api/fx/storage/mongo/store/quotes.go +++ b/api/fx/storage/mongo/store/quotes.go @@ -95,6 +95,9 @@ func (q *quotesStore) Issue(ctx context.Context, quote *model.Quote) error { expiry := time.UnixMilli(quote.ExpiresAtUnixMs) quote.ExpiresAt = &expiry } + if quote.PricedAtUnixMs <= 0 { + quote.PricedAtUnixMs = time.Now().UnixMilli() + } quote.Status = model.QuoteStatusIssued quote.ConsumedByLedgerTxnRef = "" diff --git a/api/fx/storage/mongo/store/quotes_test.go b/api/fx/storage/mongo/store/quotes_test.go index d65779d6..efdec5a3 100644 --- a/api/fx/storage/mongo/store/quotes_test.go +++ b/api/fx/storage/mongo/store/quotes_test.go @@ -32,6 +32,9 @@ func TestQuotesStoreIssue(t *testing.T) { if inserted == nil || inserted.Status != model.QuoteStatusIssued { t.Fatalf("expected issued quote to be inserted") } + if inserted.PricedAtUnixMs <= 0 { + t.Fatalf("expected pricedAtUnixMs to be populated") + } } func TestQuotesStoreIssueSetsExpiryDate(t *testing.T) { diff --git a/api/gateway/chain/go.mod b/api/gateway/chain/go.mod index bdf94ce7..53abec77 100644 --- a/api/gateway/chain/go.mod +++ b/api/gateway/chain/go.mod @@ -15,14 +15,14 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 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-20260212005555-3a7e5700f354 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect diff --git a/api/gateway/chain/go.sum b/api/gateway/chain/go.sum index 0adbce5a..be993286 100644 --- a/api/gateway/chain/go.sum +++ b/api/gateway/chain/go.sum @@ -6,8 +6,8 @@ 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-20260212005555-3a7e5700f354 h1:BgaMXBpcqcW74afzqI3iKo07K3tC+VuyWU3/FIvLlNI= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260212005555-3a7e5700f354/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 h1:QD30TjDPWtvXb5PBZGZ6Wdvaq7HQixIBtZ/yuseNXc8= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3/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= @@ -362,8 +362,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/gateway/mntx/go.mod b/api/gateway/mntx/go.mod index 6bdedf08..0bfa7825 100644 --- a/api/gateway/mntx/go.mod +++ b/api/gateway/mntx/go.mod @@ -11,7 +11,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/gateway/mntx/go.sum b/api/gateway/mntx/go.sum index 546641fd..5cf7861e 100644 --- a/api/gateway/mntx/go.sum +++ b/api/gateway/mntx/go.sum @@ -212,8 +212,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/gateway/tgsettle/go.mod b/api/gateway/tgsettle/go.mod index 991c6fff..e3eab7da 100644 --- a/api/gateway/tgsettle/go.mod +++ b/api/gateway/tgsettle/go.mod @@ -8,7 +8,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/gateway/tgsettle/go.sum b/api/gateway/tgsettle/go.sum index 35825f0b..3552c709 100644 --- a/api/gateway/tgsettle/go.sum +++ b/api/gateway/tgsettle/go.sum @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/gateway/tron/go.mod b/api/gateway/tron/go.mod index aaa406d1..a7cb8a06 100644 --- a/api/gateway/tron/go.mod +++ b/api/gateway/tron/go.mod @@ -17,14 +17,14 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 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-20260212005555-3a7e5700f354 // indirect + github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 // indirect github.com/beorn7/perks v1.0.1 // indirect github.com/bits-and-blooms/bitset v1.24.4 // indirect github.com/bmatcuk/doublestar/v4 v4.10.0 // indirect diff --git a/api/gateway/tron/go.sum b/api/gateway/tron/go.sum index 21f0b502..7b0b6710 100644 --- a/api/gateway/tron/go.sum +++ b/api/gateway/tron/go.sum @@ -6,8 +6,8 @@ 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-20260212005555-3a7e5700f354 h1:BgaMXBpcqcW74afzqI3iKo07K3tC+VuyWU3/FIvLlNI= -github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260212005555-3a7e5700f354/go.mod h1:ioLG6R+5bUSO1oeGSDxOV3FADARuMoytZCSX6MEMQkI= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3 h1:QD30TjDPWtvXb5PBZGZ6Wdvaq7HQixIBtZ/yuseNXc8= +github.com/ProjectZKM/Ziren/crates/go-runtime/zkvm_runtime v0.0.0-20260213131322-086e44a26cf3/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= @@ -383,8 +383,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/ledger/go.mod b/api/ledger/go.mod index 4c30bd16..91f63e60 100644 --- a/api/ledger/go.mod +++ b/api/ledger/go.mod @@ -11,7 +11,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/ledger/go.sum b/api/ledger/go.sum index 80161577..fd724be9 100644 --- a/api/ledger/go.sum +++ b/api/ledger/go.sum @@ -212,8 +212,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/notification/go.mod b/api/notification/go.mod index ef296a3f..7766b98c 100644 --- a/api/notification/go.mod +++ b/api/notification/go.mod @@ -51,6 +51,6 @@ require ( golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/grpc v1.79.0 // indirect + google.golang.org/grpc v1.79.1 // indirect google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/notification/go.sum b/api/notification/go.sum index 8246be3b..1e2c6e6b 100644 --- a/api/notification/go.sum +++ b/api/notification/go.sum @@ -227,8 +227,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/methods/go.mod b/api/payments/methods/go.mod index 75040980..2980ab15 100644 --- a/api/payments/methods/go.mod +++ b/api/payments/methods/go.mod @@ -11,7 +11,8 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 + google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) @@ -48,5 +49,4 @@ require ( golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 // indirect - google.golang.org/protobuf v1.36.11 // indirect ) diff --git a/api/payments/methods/go.sum b/api/payments/methods/go.sum index 35825f0b..3552c709 100644 --- a/api/payments/methods/go.sum +++ b/api/payments/methods/go.sum @@ -210,8 +210,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/methods/internal/service/methods/create.go b/api/payments/methods/internal/service/methods/create.go index 4f5eb58c..7753e584 100644 --- a/api/payments/methods/internal/service/methods/create.go +++ b/api/payments/methods/internal/service/methods/create.go @@ -24,7 +24,7 @@ func (s *Service) CreatePaymentMethod(ctx context.Context, req *methodsv1.Create return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) } - pm, err := decodePaymentMethod(req.GetPaymentMethodJson()) + pm, err := decodePaymentMethodPayload(req.GetPaymentMethod(), "payment_method") if err != nil { return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) } @@ -32,10 +32,10 @@ func (s *Service) CreatePaymentMethod(ctx context.Context, req *methodsv1.Create return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) } - payload, err := encodePaymentMethod(pm) + record, err := encodePaymentMethodRecord(pm) if err != nil { return autoError[methodsv1.CreatePaymentMethodResponse](ctx, s.logger, err) } - return &methodsv1.CreatePaymentMethodResponse{PaymentMethodJson: payload}, nil + return &methodsv1.CreatePaymentMethodResponse{PaymentMethodRecord: record}, nil } diff --git a/api/payments/methods/internal/service/methods/get.go b/api/payments/methods/internal/service/methods/get.go index d014767a..64da7171 100644 --- a/api/payments/methods/internal/service/methods/get.go +++ b/api/payments/methods/internal/service/methods/get.go @@ -29,10 +29,10 @@ func (s *Service) GetPaymentMethod(ctx context.Context, req *methodsv1.GetPaymen return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err) } - payload, err := encodePaymentMethod(pm) + record, err := encodePaymentMethodRecord(pm) if err != nil { return autoError[methodsv1.GetPaymentMethodResponse](ctx, s.logger, err) } - return &methodsv1.GetPaymentMethodResponse{PaymentMethodJson: payload}, nil + return &methodsv1.GetPaymentMethodResponse{PaymentMethodRecord: record}, nil } diff --git a/api/payments/methods/internal/service/methods/list.go b/api/payments/methods/internal/service/methods/list.go index 698406ce..48d702d7 100644 --- a/api/payments/methods/internal/service/methods/list.go +++ b/api/payments/methods/internal/service/methods/list.go @@ -4,6 +4,7 @@ import ( "context" "github.com/tech/sendico/pkg/merrors" + endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1" methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" ) @@ -33,16 +34,16 @@ func (s *Service) ListPaymentMethods(ctx context.Context, req *methodsv1.ListPay return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) } - result := make([][]byte, 0, len(items)) + result := make([]*endpointv1.PaymentMethodRecord, 0, len(items)) for i := range items { - payload, err := encodePaymentMethod(&items[i]) + record, err := encodePaymentMethodRecord(&items[i]) if err != nil { return autoError[methodsv1.ListPaymentMethodsResponse](ctx, s.logger, err) } - result = append(result, payload) + result = append(result, record) } return &methodsv1.ListPaymentMethodsResponse{ - PaymentMethodsJson: result, + PaymentMethods: result, }, nil } diff --git a/api/payments/methods/internal/service/methods/update.go b/api/payments/methods/internal/service/methods/update.go index 2f880c51..d136fb00 100644 --- a/api/payments/methods/internal/service/methods/update.go +++ b/api/payments/methods/internal/service/methods/update.go @@ -20,7 +20,7 @@ func (s *Service) UpdatePaymentMethod(ctx context.Context, req *methodsv1.Update return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) } - pm, err := decodePaymentMethod(req.GetPaymentMethodJson()) + pm, err := decodePaymentMethodRecord(req.GetPaymentMethodRecord()) if err != nil { return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) } @@ -28,10 +28,10 @@ func (s *Service) UpdatePaymentMethod(ctx context.Context, req *methodsv1.Update return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) } - payload, err := encodePaymentMethod(pm) + record, err := encodePaymentMethodRecord(pm) if err != nil { return autoError[methodsv1.UpdatePaymentMethodResponse](ctx, s.logger, err) } - return &methodsv1.UpdatePaymentMethodResponse{PaymentMethodJson: payload}, nil + return &methodsv1.UpdatePaymentMethodResponse{PaymentMethodRecord: record}, nil } diff --git a/api/payments/methods/internal/service/methods/util.go b/api/payments/methods/internal/service/methods/util.go index da1d1fac..25496261 100644 --- a/api/payments/methods/internal/service/methods/util.go +++ b/api/payments/methods/internal/service/methods/util.go @@ -2,17 +2,24 @@ package methods import ( "context" - "encoding/json" "fmt" "strings" + "time" "github.com/tech/sendico/pkg/api/routers/gsresponse" "github.com/tech/sendico/pkg/merrors" "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" + archivablev1 "github.com/tech/sendico/pkg/proto/common/archivable/v1" + describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" + oboundv1 "github.com/tech/sendico/pkg/proto/common/organization_bound/v1" paginationv2 "github.com/tech/sendico/pkg/proto/common/pagination/v2" + pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1" + storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1" + endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1" "go.mongodb.org/mongo-driver/v2/bson" + "google.golang.org/protobuf/types/known/timestamppb" ) func autoError[T any](ctx context.Context, logger mlogger.Logger, err error) (*T, error) { @@ -31,26 +38,228 @@ func parseObjectID(value, field string) (bson.ObjectID, error) { return ref, nil } -func decodePaymentMethod(data []byte) (*model.PaymentMethod, error) { - if len(data) == 0 { - return nil, merrors.InvalidArgument("payment_method_json is required", "payment_method_json") +func decodePaymentMethodRecord(record *endpointv1.PaymentMethodRecord) (*model.PaymentMethod, error) { + if record == nil { + return nil, merrors.InvalidArgument("payment_method_record is required", "payment_method_record") } - res := &model.PaymentMethod{} - if err := json.Unmarshal(data, res); err != nil { - return nil, merrors.InvalidArgumentWrap(err, "failed to decode payment method", "payment_method_json") + res, err := decodePaymentMethodPayload(record.GetPaymentMethod(), "payment_method_record.payment_method") + if err != nil { + return nil, err + } + if err := applyPermissionBoundRecord(res, record.GetPermissionBound()); err != nil { + return nil, err } return res, nil } -func encodePaymentMethod(pm *model.PaymentMethod) ([]byte, error) { +func decodePaymentMethodPayload(method *endpointv1.PaymentMethod, field string) (*model.PaymentMethod, error) { + if method == nil { + return nil, merrors.InvalidArgument(field+" is required", field) + } + + recipientRef, err := parseObjectID(method.GetRecipientRef(), field+".recipient_ref") + if err != nil { + return nil, err + } + pt, err := paymentTypeFromProto(method.GetType(), field+".type") + if err != nil { + return nil, err + } + + return &model.PaymentMethod{ + Describable: describableFromProto(method.GetDescribable()), + RecipientRef: recipientRef, + Type: pt, + Data: cloneBytes(method.GetData()), + IsMain: method.GetIsMain(), + }, nil +} + +func encodePaymentMethodRecord(pm *model.PaymentMethod) (*endpointv1.PaymentMethodRecord, error) { if pm == nil { return nil, merrors.InvalidArgument("payment method is required") } - payload, err := json.Marshal(pm) + pt, err := paymentTypeToProto(pm.Type) if err != nil { - return nil, merrors.InternalWrap(err, "failed to encode payment method") + return nil, err } - return payload, nil + + return &endpointv1.PaymentMethodRecord{ + PermissionBound: permissionBoundFromModel(pm), + PaymentMethod: &endpointv1.PaymentMethod{ + Describable: describableToProto(pm.Describable), + RecipientRef: toObjectHex(pm.RecipientRef), + Type: pt, + Data: cloneBytes(pm.Data), + IsMain: pm.IsMain, + }, + }, nil +} + +func paymentTypeFromProto(value endpointv1.PaymentMethodType, field string) (model.PaymentType, error) { + switch value { + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN: + return model.PaymentTypeIban, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD: + return model.PaymentTypeCard, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN: + return model.PaymentTypeCardToken, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT: + return model.PaymentTypeBankAccount, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET: + return model.PaymentTypeWallet, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS: + return model.PaymentTypeCryptoAddress, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER: + return model.PaymentTypeLedger, nil + default: + return model.PaymentTypeIban, merrors.InvalidArgument(fmt.Sprintf("%s has unsupported value: %s", field, value.String()), field) + } +} + +func paymentTypeToProto(value model.PaymentType) (endpointv1.PaymentMethodType, error) { + switch value { + case model.PaymentTypeIban: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN, nil + case model.PaymentTypeCard: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, nil + case model.PaymentTypeCardToken: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, nil + case model.PaymentTypeBankAccount: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT, nil + case model.PaymentTypeWallet: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, nil + case model.PaymentTypeCryptoAddress: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, nil + case model.PaymentTypeLedger: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, nil + default: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported payment method type: %s", value.String()), "type") + } +} + +func describableFromProto(src *describablev1.Describable) model.Describable { + if src == nil { + return model.Describable{} + } + res := model.Describable{Name: src.GetName()} + if src.Description != nil { + v := src.GetDescription() + res.Description = &v + } + return res +} + +func describableToProto(src model.Describable) *describablev1.Describable { + if strings.TrimSpace(src.Name) == "" && src.Description == nil { + return nil + } + res := &describablev1.Describable{ + Name: src.Name, + } + if src.Description != nil { + v := *src.Description + res.Description = &v + } + return res +} + +func cloneBytes(src []byte) []byte { + if len(src) == 0 { + return nil + } + dst := make([]byte, len(src)) + copy(dst, src) + return dst +} + +func applyPermissionBoundRecord(pm *model.PaymentMethod, src *pboundv1.PermissionBound) error { + if pm == nil || src == nil { + return nil + } + + if storable := src.GetStorable(); storable != nil { + if methodRef, err := parseOptionalObjectID(storable.GetId(), "payment_method_record.permission_bound.storable.id"); err != nil { + return err + } else if methodRef != bson.NilObjectID { + pm.ID = methodRef + } + pm.CreatedAt = fromProtoTime(storable.GetCreatedAt()) + pm.UpdatedAt = fromProtoTime(storable.GetUpdatedAt()) + } + + if archivable := src.GetArchivable(); archivable != nil { + pm.Archived = archivable.GetIsArchived() + } + + if orgBound := src.GetOrganizationBound(); orgBound != nil { + if orgRef, err := parseOptionalObjectID(orgBound.GetOrganizationRef(), "payment_method_record.permission_bound.organization_bound.organization_ref"); err != nil { + return err + } else if orgRef != bson.NilObjectID { + pm.SetOrganizationRef(orgRef) + } + } + + if permissionRef, err := parseOptionalObjectID(src.GetPermissionRef(), "payment_method_record.permission_bound.permission_ref"); err != nil { + return err + } else if permissionRef != bson.NilObjectID { + pm.SetPermissionRef(permissionRef) + } + + return nil +} + +func permissionBoundFromModel(pm *model.PaymentMethod) *pboundv1.PermissionBound { + if pm == nil { + return nil + } + return &pboundv1.PermissionBound{ + Storable: &storablev1.Storable{ + Id: toObjectHex(pm.ID), + CreatedAt: toProtoTime(pm.CreatedAt), + UpdatedAt: toProtoTime(pm.UpdatedAt), + }, + Archivable: &archivablev1.Archivable{ + IsArchived: pm.Archived, + }, + OrganizationBound: &oboundv1.OrganizationBound{ + OrganizationRef: toObjectHex(pm.GetOrganizationRef()), + }, + PermissionRef: toObjectHex(pm.GetPermissionRef()), + } +} + +func parseOptionalObjectID(value, field string) (bson.ObjectID, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return bson.NilObjectID, nil + } + ref, err := bson.ObjectIDFromHex(trimmed) + if err != nil { + return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s must be a valid object id", field), field) + } + return ref, nil +} + +func toObjectHex(value bson.ObjectID) string { + if value == bson.NilObjectID { + return "" + } + return value.Hex() +} + +func toProtoTime(value time.Time) *timestamppb.Timestamp { + if value.IsZero() { + return nil + } + return timestamppb.New(value) +} + +func fromProtoTime(value *timestamppb.Timestamp) time.Time { + if value == nil { + return time.Time{} + } + return value.AsTime() } func toModelCursor(cursor *paginationv2.ViewCursor) *model.ViewCursor { diff --git a/api/payments/orchestrator/go.mod b/api/payments/orchestrator/go.mod index f30ae969..97632f98 100644 --- a/api/payments/orchestrator/go.mod +++ b/api/payments/orchestrator/go.mod @@ -28,7 +28,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/payments/orchestrator/go.sum b/api/payments/orchestrator/go.sum index 07e82f8b..db932601 100644 --- a/api/payments/orchestrator/go.sum +++ b/api/payments/orchestrator/go.sum @@ -213,8 +213,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/orchestrator/internal/service/orchestrator/convert.go b/api/payments/orchestrator/internal/service/orchestrator/convert.go index 6aa8ea7d..3516d077 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/convert.go +++ b/api/payments/orchestrator/internal/service/orchestrator/convert.go @@ -685,6 +685,10 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { if quote == nil { return nil } + pricedAtUnixMs := int64(0) + if ts := quote.GetPricedAt(); ts != nil { + pricedAtUnixMs = ts.AsTime().UnixMilli() + } return &paymenttypes.FXQuote{ QuoteRef: strings.TrimSpace(quote.GetQuoteRef()), Pair: pairFromProto(quote.GetPair()), @@ -693,6 +697,7 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { BaseAmount: moneyFromProto(quote.GetBaseAmount()), QuoteAmount: moneyFromProto(quote.GetQuoteAmount()), ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(), + PricedAtUnixMs: pricedAtUnixMs, Provider: strings.TrimSpace(quote.GetProvider()), RateRef: strings.TrimSpace(quote.GetRateRef()), Firm: quote.GetFirm(), @@ -703,6 +708,10 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { if quote == nil { return nil } + var pricedAt *timestamppb.Timestamp + if quote.PricedAtUnixMs > 0 { + pricedAt = timestamppb.New(time.UnixMilli(quote.PricedAtUnixMs).UTC()) + } return &oraclev1.Quote{ QuoteRef: strings.TrimSpace(quote.QuoteRef), Pair: pairToProto(quote.Pair), @@ -711,6 +720,7 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { BaseAmount: protoMoney(quote.BaseAmount), QuoteAmount: protoMoney(quote.QuoteAmount), ExpiresAtUnixMs: quote.ExpiresAtUnixMs, + PricedAt: pricedAt, Provider: strings.TrimSpace(quote.Provider), RateRef: strings.TrimSpace(quote.RateRef), Firm: quote.Firm, diff --git a/api/payments/orchestrator/internal/service/orchestrator/convert_types_test.go b/api/payments/orchestrator/internal/service/orchestrator/convert_types_test.go index 4362a380..40fc62b5 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/convert_types_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/convert_types_test.go @@ -2,6 +2,7 @@ package orchestrator import ( "testing" + "time" paymenttypes "github.com/tech/sendico/pkg/payments/types" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" @@ -10,6 +11,7 @@ import ( moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) func TestMoneyConversionRoundTrip(t *testing.T) { @@ -69,6 +71,7 @@ func TestFeeLineConversionRoundTrip(t *testing.T) { } func TestFXQuoteConversionRoundTrip(t *testing.T) { + pricedAt := int64(1700000000000) proto := &oraclev1.Quote{ QuoteRef: "q1", Pair: &fxv1.CurrencyPair{Base: "USD", Quote: "EUR"}, @@ -77,6 +80,7 @@ func TestFXQuoteConversionRoundTrip(t *testing.T) { BaseAmount: &moneyv1.Money{Currency: "USD", Amount: "100"}, QuoteAmount: &moneyv1.Money{Currency: "EUR", Amount: "90"}, ExpiresAtUnixMs: 1700000000000, + PricedAt: timestamppb.New(time.UnixMilli(pricedAt).UTC()), Provider: "provider", RateRef: "rate", Firm: true, @@ -88,6 +92,9 @@ func TestFXQuoteConversionRoundTrip(t *testing.T) { if model.Side != paymenttypes.FXSideSellBaseBuyQuote || model.Price.GetValue() != "0.9" { t.Fatalf("fxQuoteFromProto enums mismatch: %#v", model) } + if model.PricedAtUnixMs != pricedAt { + t.Fatalf("fxQuoteFromProto priced_at mismatch: %#v", model) + } back := fxQuoteToProto(model) if back == nil || back.GetQuoteRef() != "q1" || back.GetPair().GetBase() != "USD" || back.GetPair().GetQuote() != "EUR" { t.Fatalf("fxQuoteToProto mismatch: %#v", back) @@ -95,6 +102,9 @@ func TestFXQuoteConversionRoundTrip(t *testing.T) { if back.GetSide() != fxv1.Side_SELL_BASE_BUY_QUOTE || back.GetPrice().GetValue() != "0.9" { t.Fatalf("fxQuoteToProto enums mismatch: %#v", back) } + if got := back.GetPricedAt(); got == nil || got.AsTime().UnixMilli() != pricedAt { + t.Fatalf("fxQuoteToProto priced_at mismatch: %#v", back) + } } func TestAssetConversionRoundTrip(t *testing.T) { diff --git a/api/payments/orchestrator/internal/service/orchestrator/payment_plan_storage.go b/api/payments/orchestrator/internal/service/orchestrator/payment_plan_storage.go index a5b6eda1..c77fb794 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/payment_plan_storage.go +++ b/api/payments/orchestrator/internal/service/orchestrator/payment_plan_storage.go @@ -69,6 +69,7 @@ func cloneStoredFXQuote(src *paymenttypes.FXQuote) *paymenttypes.FXQuote { QuoteRef: strings.TrimSpace(src.QuoteRef), Side: src.Side, ExpiresAtUnixMs: src.ExpiresAtUnixMs, + PricedAtUnixMs: src.PricedAtUnixMs, Provider: strings.TrimSpace(src.Provider), RateRef: strings.TrimSpace(src.RateRef), Firm: src.Firm, diff --git a/api/payments/orchestrator/internal/service/plan_builder/helpers.go b/api/payments/orchestrator/internal/service/plan_builder/helpers.go index 667d7e1a..865eaa6f 100644 --- a/api/payments/orchestrator/internal/service/plan_builder/helpers.go +++ b/api/payments/orchestrator/internal/service/plan_builder/helpers.go @@ -2,6 +2,7 @@ package plan_builder import ( "strings" + "time" "github.com/shopspring/decimal" "github.com/tech/sendico/payments/storage/model" @@ -15,6 +16,7 @@ import ( chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) type moneyGetter interface { @@ -226,6 +228,10 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { if quote == nil { return nil } + pricedAtUnixMs := int64(0) + if ts := quote.GetPricedAt(); ts != nil { + pricedAtUnixMs = ts.AsTime().UnixMilli() + } return &paymenttypes.FXQuote{ QuoteRef: strings.TrimSpace(quote.GetQuoteRef()), Pair: pairFromProto(quote.GetPair()), @@ -234,6 +240,7 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { BaseAmount: moneyFromProto(quote.GetBaseAmount()), QuoteAmount: moneyFromProto(quote.GetQuoteAmount()), ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(), + PricedAtUnixMs: pricedAtUnixMs, Provider: strings.TrimSpace(quote.GetProvider()), RateRef: strings.TrimSpace(quote.GetRateRef()), Firm: quote.GetFirm(), @@ -244,6 +251,10 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { if quote == nil { return nil } + var pricedAt *timestamppb.Timestamp + if quote.PricedAtUnixMs > 0 { + pricedAt = timestamppb.New(time.UnixMilli(quote.PricedAtUnixMs).UTC()) + } return &oraclev1.Quote{ QuoteRef: strings.TrimSpace(quote.QuoteRef), Pair: pairToProto(quote.Pair), @@ -252,6 +263,7 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { BaseAmount: protoMoney(quote.BaseAmount), QuoteAmount: protoMoney(quote.QuoteAmount), ExpiresAtUnixMs: quote.ExpiresAtUnixMs, + PricedAt: pricedAt, Provider: strings.TrimSpace(quote.Provider), RateRef: strings.TrimSpace(quote.RateRef), Firm: quote.Firm, diff --git a/api/payments/quotation/go.mod b/api/payments/quotation/go.mod index eb998142..8da6fa14 100644 --- a/api/payments/quotation/go.mod +++ b/api/payments/quotation/go.mod @@ -26,7 +26,7 @@ require ( github.com/tech/sendico/pkg v0.1.0 go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 ) diff --git a/api/payments/quotation/go.sum b/api/payments/quotation/go.sum index c4d799eb..470bab37 100644 --- a/api/payments/quotation/go.sum +++ b/api/payments/quotation/go.sum @@ -213,8 +213,8 @@ 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-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/payments/quotation/internal/service/plan/helpers.go b/api/payments/quotation/internal/service/plan/helpers.go index e9a5ccbf..127f84e6 100644 --- a/api/payments/quotation/internal/service/plan/helpers.go +++ b/api/payments/quotation/internal/service/plan/helpers.go @@ -2,6 +2,7 @@ package plan import ( "strings" + "time" "github.com/shopspring/decimal" "github.com/tech/sendico/payments/storage/model" @@ -13,6 +14,7 @@ import ( moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) type moneyGetter interface { @@ -158,6 +160,10 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { if quote == nil { return nil } + pricedAtUnixMs := int64(0) + if ts := quote.GetPricedAt(); ts != nil { + pricedAtUnixMs = ts.AsTime().UnixMilli() + } return &paymenttypes.FXQuote{ QuoteRef: strings.TrimSpace(quote.GetQuoteRef()), Pair: pairFromProto(quote.GetPair()), @@ -166,6 +172,7 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { BaseAmount: moneyFromProto(quote.GetBaseAmount()), QuoteAmount: moneyFromProto(quote.GetQuoteAmount()), ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(), + PricedAtUnixMs: pricedAtUnixMs, Provider: strings.TrimSpace(quote.GetProvider()), RateRef: strings.TrimSpace(quote.GetRateRef()), Firm: quote.GetFirm(), @@ -176,6 +183,10 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { if quote == nil { return nil } + var pricedAt *timestamppb.Timestamp + if quote.PricedAtUnixMs > 0 { + pricedAt = timestamppb.New(time.UnixMilli(quote.PricedAtUnixMs).UTC()) + } return &oraclev1.Quote{ QuoteRef: strings.TrimSpace(quote.QuoteRef), Pair: pairToProto(quote.Pair), @@ -184,6 +195,7 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { BaseAmount: protoMoney(quote.BaseAmount), QuoteAmount: protoMoney(quote.QuoteAmount), ExpiresAtUnixMs: quote.ExpiresAtUnixMs, + PricedAt: pricedAt, Provider: strings.TrimSpace(quote.Provider), RateRef: strings.TrimSpace(quote.RateRef), Firm: quote.Firm, diff --git a/api/payments/quotation/internal/service/quotation/convert.go b/api/payments/quotation/internal/service/quotation/convert.go index 719d5af4..5d31a541 100644 --- a/api/payments/quotation/internal/service/quotation/convert.go +++ b/api/payments/quotation/internal/service/quotation/convert.go @@ -2,6 +2,7 @@ package quotation import ( "strings" + "time" "github.com/tech/sendico/payments/storage/model" chainasset "github.com/tech/sendico/pkg/chain" @@ -13,6 +14,7 @@ import ( chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" + "google.golang.org/protobuf/types/known/timestamppb" ) func intentFromProto(src *sharedv1.PaymentIntent) model.PaymentIntent { @@ -423,6 +425,10 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { if quote == nil { return nil } + pricedAtUnixMs := int64(0) + if ts := quote.GetPricedAt(); ts != nil { + pricedAtUnixMs = ts.AsTime().UnixMilli() + } return &paymenttypes.FXQuote{ QuoteRef: strings.TrimSpace(quote.GetQuoteRef()), Pair: pairFromProto(quote.GetPair()), @@ -431,6 +437,7 @@ func fxQuoteFromProto(quote *oraclev1.Quote) *paymenttypes.FXQuote { BaseAmount: moneyFromProto(quote.GetBaseAmount()), QuoteAmount: moneyFromProto(quote.GetQuoteAmount()), ExpiresAtUnixMs: quote.GetExpiresAtUnixMs(), + PricedAtUnixMs: pricedAtUnixMs, Provider: strings.TrimSpace(quote.GetProvider()), RateRef: strings.TrimSpace(quote.GetRateRef()), Firm: quote.GetFirm(), @@ -441,6 +448,10 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { if quote == nil { return nil } + var pricedAt *timestamppb.Timestamp + if quote.PricedAtUnixMs > 0 { + pricedAt = timestamppb.New(time.UnixMilli(quote.PricedAtUnixMs).UTC()) + } return &oraclev1.Quote{ QuoteRef: strings.TrimSpace(quote.QuoteRef), Pair: pairToProto(quote.Pair), @@ -449,6 +460,7 @@ func fxQuoteToProto(quote *paymenttypes.FXQuote) *oraclev1.Quote { BaseAmount: protoMoney(quote.BaseAmount), QuoteAmount: protoMoney(quote.QuoteAmount), ExpiresAtUnixMs: quote.ExpiresAtUnixMs, + PricedAt: pricedAt, Provider: strings.TrimSpace(quote.Provider), RateRef: strings.TrimSpace(quote.RateRef), Firm: quote.Firm, diff --git a/api/payments/quotation/internal/service/quotation/helpers.go b/api/payments/quotation/internal/service/quotation/helpers.go index 07e86cee..4389fa22 100644 --- a/api/payments/quotation/internal/service/quotation/helpers.go +++ b/api/payments/quotation/internal/service/quotation/helpers.go @@ -10,6 +10,7 @@ import ( oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" "google.golang.org/protobuf/proto" + "google.golang.org/protobuf/types/known/timestamppb" feesv1 "github.com/tech/sendico/pkg/proto/billing/fees/v1" accountingv1 "github.com/tech/sendico/pkg/proto/common/accounting/v1" @@ -293,6 +294,10 @@ func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote { if src == nil { return nil } + var pricedAt *timestamppb.Timestamp + if !src.PricedAt.IsZero() { + pricedAt = timestamppb.New(src.PricedAt.UTC()) + } return &oraclev1.Quote{ QuoteRef: src.QuoteRef, Pair: src.Pair, @@ -301,6 +306,7 @@ func quoteToProto(src *oracleclient.Quote) *oraclev1.Quote { BaseAmount: cloneProtoMoney(src.BaseAmount), QuoteAmount: cloneProtoMoney(src.QuoteAmount), ExpiresAtUnixMs: src.ExpiresAt.UnixMilli(), + PricedAt: pricedAt, Provider: src.Provider, RateRef: src.RateRef, Firm: src.Firm, diff --git a/api/payments/quotation/internal/service/quotation/payment_plan_factory.go b/api/payments/quotation/internal/service/quotation/payment_plan_factory.go index c36083d7..2a31e594 100644 --- a/api/payments/quotation/internal/service/quotation/payment_plan_factory.go +++ b/api/payments/quotation/internal/service/quotation/payment_plan_factory.go @@ -119,6 +119,7 @@ func cloneStoredFXQuote(src *paymenttypes.FXQuote) *paymenttypes.FXQuote { QuoteRef: strings.TrimSpace(src.QuoteRef), Side: src.Side, ExpiresAtUnixMs: src.ExpiresAtUnixMs, + PricedAtUnixMs: src.PricedAtUnixMs, Provider: strings.TrimSpace(src.Provider), RateRef: strings.TrimSpace(src.RateRef), Firm: src.Firm, diff --git a/api/pkg/go.mod b/api/pkg/go.mod index ce46df58..f2ddc820 100644 --- a/api/pkg/go.mod +++ b/api/pkg/go.mod @@ -17,7 +17,7 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 golang.org/x/crypto v0.48.0 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 ) diff --git a/api/pkg/go.sum b/api/pkg/go.sum index cc8962ea..d129e3dc 100644 --- a/api/pkg/go.sum +++ b/api/pkg/go.sum @@ -273,8 +273,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217 h1: google.golang.org/genproto/googleapis/api v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:+rXWjjaukWZun3mLfjmVnQi18E1AsFbDN9QdJ5YXLto= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/pkg/payments/types/fx.go b/api/pkg/payments/types/fx.go index 3d4cb81a..9399e248 100644 --- a/api/pkg/payments/types/fx.go +++ b/api/pkg/payments/types/fx.go @@ -38,6 +38,7 @@ type FXQuote struct { BaseAmount *Money `bson:"baseAmount,omitempty" json:"baseAmount,omitempty"` QuoteAmount *Money `bson:"quoteAmount,omitempty" json:"quoteAmount,omitempty"` ExpiresAtUnixMs int64 `bson:"expiresAtUnixMs,omitempty" json:"expiresAtUnixMs,omitempty"` + PricedAtUnixMs int64 `bson:"pricedAtUnixMs,omitempty" json:"pricedAtUnixMs,omitempty"` Provider string `bson:"provider,omitempty" json:"provider,omitempty"` RateRef string `bson:"rateRef,omitempty" json:"rateRef,omitempty"` Firm bool `bson:"firm,omitempty" json:"firm,omitempty"` @@ -85,6 +86,13 @@ func (q *FXQuote) GetExpiresAtUnixMs() int64 { return q.ExpiresAtUnixMs } +func (q *FXQuote) GetPricedAtUnixMs() int64 { + if q == nil { + return 0 + } + return q.PricedAtUnixMs +} + func (q *FXQuote) GetProvider() string { if q == nil { return "" diff --git a/api/proto/billing/fees/v1/fees.proto b/api/proto/billing/fees/v1/fees.proto index c9d29c5b..4b5e051d 100644 --- a/api/proto/billing/fees/v1/fees.proto +++ b/api/proto/billing/fees/v1/fees.proto @@ -5,10 +5,10 @@ package fees.v1; option go_package = "github.com/tech/sendico/pkg/proto/billing/fees/v1;feesv1"; import "google/protobuf/timestamp.proto"; -import "common/money/v1/money.proto"; -import "common/fx/v1/fx.proto"; -import "common/accounting/v1/posting.proto"; -import "common/trace/v1/trace.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/fx/v1/fx.proto"; +import "api/proto/common/accounting/v1/posting.proto"; +import "api/proto/common/trace/v1/trace.proto"; // -------------------- // Core business enums @@ -74,10 +74,10 @@ message FXUsed { // A derived posting line ready for the ledger to post as-is. message DerivedPostingLine { - string ledger_account_ref = 1; // resolved account - common.money.v1.Money money = 2; // amount/currency - common.accounting.v1.PostingLineType line_type = 3; // FEE/TAX/SPREAD/REVERSAL - common.accounting.v1.EntrySide side = 4; // DEBIT/CREDIT + string ledger_account_ref = 1; // resolved account + common.money.v1.Money money = 2; // amount/currency + common.accounting.v1.PostingLineType line_type = 3; // FEE/TAX/SPREAD/REVERSAL + common.accounting.v1.EntrySide side = 4; // DEBIT/CREDIT map meta = 5; // fee_rule_id, rule_version, tax_code, tax_rate, fx_rate_used, etc. } @@ -87,8 +87,8 @@ message AppliedRule { string rule_version = 2; string formula = 3; // e.g., "2.90% + 0.30 (min 0.50)" common.money.v1.RoundingMode rounding = 4; - string tax_code = 5; // if applicable - string tax_rate = 6; // decimal string + string tax_code = 5; // if applicable + string tax_rate = 6; // decimal string map parameters = 7; // thresholds, tiers, etc. } diff --git a/api/proto/common/archivable/v1/archivable.proto b/api/proto/common/archivable/v1/archivable.proto new file mode 100644 index 00000000..2ba21733 --- /dev/null +++ b/api/proto/common/archivable/v1/archivable.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package common.archivable.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/archivable/v1;archivablev1"; + + + +message Archivable { + bool is_archived = 1; +} \ No newline at end of file diff --git a/api/proto/common/gateway/v1/gateway.proto b/api/proto/common/gateway/v1/gateway.proto index 83da719f..a163508c 100644 --- a/api/proto/common/gateway/v1/gateway.proto +++ b/api/proto/common/gateway/v1/gateway.proto @@ -1,8 +1,11 @@ syntax = "proto3"; + package common.gateway.v1; + option go_package = "github.com/tech/sendico/pkg/proto/common/gateway/v1;gatewayv1"; -import "common/money/v1/money.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/payments/endpoint/v1/endpoint.proto"; enum Operation { @@ -18,17 +21,6 @@ enum Operation { OPERATION_CREATE_ACCOUNT = 9; } -enum PaymentMethodType { - PM_UNSPECIFIED = 0; - PM_CARD = 1; - PM_SEPA = 2; - PM_ACH = 3; - PM_PIX = 4; - PM_WALLET = 5; - PM_CRYPTO = 6; - PM_LOCAL_BANK = 7; // generic local rails, refine later if needed -} - // Rail identifiers for orchestration. Extend with new rails as needed. enum Rail { RAIL_UNSPECIFIED = 0; @@ -70,7 +62,7 @@ message OperationCapabilities { // Per-method matrix entry message MethodCapability { - PaymentMethodType method = 1; + payments.endpoint.v1.PaymentMethod method = 1; // ISO 4217 currency codes, e.g. "EUR", "USD" repeated string currencies = 2; @@ -187,4 +179,4 @@ message OperationExecutionStatus { common.money.v1.Money executed_money = 3; OperationResult status = 4; OperationError error = 5; -} \ No newline at end of file +} diff --git a/api/proto/common/organization_bound/v1/obound.proto b/api/proto/common/organization_bound/v1/obound.proto new file mode 100644 index 00000000..93670c12 --- /dev/null +++ b/api/proto/common/organization_bound/v1/obound.proto @@ -0,0 +1,10 @@ +syntax = "proto3"; + +package common.obound.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/organization_bound/v1;oboundv1"; + + +message OrganizationBound { + string organization_ref = 1; +} \ No newline at end of file diff --git a/api/proto/common/payment/v1/card.proto b/api/proto/common/payment/v1/card.proto new file mode 100644 index 00000000..6b55b168 --- /dev/null +++ b/api/proto/common/payment/v1/card.proto @@ -0,0 +1,73 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + +import "google/protobuf/wrappers.proto"; + + +// ------------------------- +// Card network (payment system) +// ------------------------- +enum CardNetwork { + CARD_NETWORK_UNSPECIFIED = 0; + CARD_NETWORK_VISA = 1; + CARD_NETWORK_MASTERCARD = 2; + CARD_NETWORK_MIR = 3; + CARD_NETWORK_AMEX = 4; + CARD_NETWORK_UNIONPAY = 5; + CARD_NETWORK_JCB = 6; + CARD_NETWORK_DISCOVER = 7; +} + +enum CardFundingType { + CARD_FUNDING_UNSPECIFIED = 0; + CARD_FUNDING_DEBIT = 1; + CARD_FUNDING_CREDIT = 2; + CARD_FUNDING_PREPAID = 3; +} + +// ------------------------- +// PCI scope: raw card details +// ------------------------- + +message RawCardData { + string pan = 1; + uint32 exp_month = 2; // 1–12 + uint32 exp_year = 3; // YYYY + string cvv = 4; // optional; often omitted for payouts +} + + +// ------------------------- +// Safe metadata (display / routing hints) +// ------------------------- +message CardMetadata { + string masked_pan = 1; // e.g. 411111******1111 + CardNetwork network = 2; // Visa/Mastercard/Mir/... + CardFundingType funding = 3; // debit/credit/prepaid (if known) + string issuing_country = 4; // ISO 3166-1 alpha-2 (if known) + string issuer_name = 5; // display only (if known) +} + + +// ------------------------- +// Card details +// Either inline credentials OR reference to stored payment method +// ------------------------- +message CardDetails { + string id = 1; + + oneof source { + RawCardData raw = 2; + string payment_method_id = 3; + } + + string cardholder_name = 4; + string cardholder_surname = 5; + + string billing_country = 6; // ISO 3166-1 alpha-2, if you need it per operation +} + + diff --git a/api/proto/common/payment/v1/custom.proto b/api/proto/common/payment/v1/custom.proto new file mode 100644 index 00000000..69278d77 --- /dev/null +++ b/api/proto/common/payment/v1/custom.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + + +message CustomPaymentDetails { + string id = 1; + bytes payment_method_json = 2; +} \ No newline at end of file diff --git a/api/proto/common/payment/v1/external_chain.proto b/api/proto/common/payment/v1/external_chain.proto new file mode 100644 index 00000000..e5a72084 --- /dev/null +++ b/api/proto/common/payment/v1/external_chain.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + +import "api/proto/gateway/chain/v1/chain.proto"; + + +message ExternalChainDetails { + string id = 1; + chain.gateway.v1.Asset asset = 2; + string address = 3; + string memo = 4; +} + diff --git a/api/proto/common/payment/v1/ledger.proto b/api/proto/common/payment/v1/ledger.proto new file mode 100644 index 00000000..e536d642 --- /dev/null +++ b/api/proto/common/payment/v1/ledger.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + + +message LedgerDetails { + string id = 1; + oneof source { + string ledger_account_ref = 2; + string account_code = 3; + } +} diff --git a/api/proto/common/payment/v1/managed_wallet.proto b/api/proto/common/payment/v1/managed_wallet.proto new file mode 100644 index 00000000..608cd780 --- /dev/null +++ b/api/proto/common/payment/v1/managed_wallet.proto @@ -0,0 +1,11 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + + +message ManagedWalletDetails { + string id = 1; + string managed_wallet_ref = 2; +} \ No newline at end of file diff --git a/api/proto/common/payment/v1/rba.proto b/api/proto/common/payment/v1/rba.proto new file mode 100644 index 00000000..e1e0af14 --- /dev/null +++ b/api/proto/common/payment/v1/rba.proto @@ -0,0 +1,16 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + +// ------------------------- +// Russian bank account details +// ------------------------- + +message RussianBankDetails { + string id = 1; + string account_number = 2; // 20 digits + string bik = 3; // 9 digits + string account_holder_name = 4; +} diff --git a/api/proto/common/payment/v1/sepa.proto b/api/proto/common/payment/v1/sepa.proto new file mode 100644 index 00000000..1293876c --- /dev/null +++ b/api/proto/common/payment/v1/sepa.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + +// ------------------------- +// SEPA bank account details +// ------------------------- + +message SepaBankDetails { + string id = 1; + string iban = 2; // IBAN + string bic = 3; // optional (BIC/SWIFT) + string account_holder_name = 4; +} + diff --git a/api/proto/common/payment/v1/settlement.proto b/api/proto/common/payment/v1/settlement.proto new file mode 100644 index 00000000..02174885 --- /dev/null +++ b/api/proto/common/payment/v1/settlement.proto @@ -0,0 +1,13 @@ + +syntax = "proto3"; + +package common.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/payment/v1;paymentv1"; + +// SettlementMode defines how to treat fees/FX variance for payouts. +enum SettlementMode { + SETTLEMENT_UNSPECIFIED = 0; + SETTLEMENT_FIX_SOURCE = 1; // customer pays fees; sent amount fixed + SETTLEMENT_FIX_RECEIVED = 2; // receiver gets fixed amount; source flexes +} diff --git a/api/proto/common/permission_bound/v1/pbound.proto b/api/proto/common/permission_bound/v1/pbound.proto new file mode 100644 index 00000000..b991fa52 --- /dev/null +++ b/api/proto/common/permission_bound/v1/pbound.proto @@ -0,0 +1,17 @@ +syntax = "proto3"; + +package common.pbound.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/permission_bound/v1;pboundv1"; + +import "api/proto/common/storable/v1/storable.proto"; +import "api/proto/common/archivable/v1/archivable.proto"; +import "api/proto/common/organization_bound/v1/obound.proto"; + + +message PermissionBound { + common.storable.v1.Storable storable = 1; + common.archivable.v1.Archivable archivable = 2; + common.obound.v1.OrganizationBound organization_bound = 3; + string permission_ref = 4; +} \ No newline at end of file diff --git a/api/proto/common/storable/v1/storable.proto b/api/proto/common/storable/v1/storable.proto new file mode 100644 index 00000000..3fee7e5b --- /dev/null +++ b/api/proto/common/storable/v1/storable.proto @@ -0,0 +1,14 @@ +syntax = "proto3"; + +package common.storable.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/common/storable/v1;storablev1"; + +import "google/protobuf/timestamp.proto"; + + +message Storable { + string id = 1; + google.protobuf.Timestamp created_at = 10; + google.protobuf.Timestamp updated_at = 11; +} diff --git a/api/proto/connector/v1/connector.proto b/api/proto/connector/v1/connector.proto index fb252e59..4735dda1 100644 --- a/api/proto/connector/v1/connector.proto +++ b/api/proto/connector/v1/connector.proto @@ -7,10 +7,10 @@ option go_package = "github.com/tech/sendico/pkg/proto/connector/v1;connectorv1" import "google/protobuf/struct.proto"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; -import "common/account_role/v1/account_role.proto"; -import "common/describable/v1/describable.proto"; -import "common/money/v1/money.proto"; -import "common/pagination/v1/cursor.proto"; +import "api/proto/common/account_role/v1/account_role.proto"; +import "api/proto/common/describable/v1/describable.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/pagination/v1/cursor.proto"; // ConnectorService exposes capability-driven account and operation primitives. service ConnectorService { diff --git a/api/proto/gateway/chain/v1/chain.proto b/api/proto/gateway/chain/v1/chain.proto index b28b484f..96e63c0a 100644 --- a/api/proto/gateway/chain/v1/chain.proto +++ b/api/proto/gateway/chain/v1/chain.proto @@ -6,9 +6,9 @@ option go_package = "github.com/tech/sendico/pkg/proto/gateway/chain/v1;chainv1" import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; -import "common/money/v1/money.proto"; -import "common/pagination/v1/cursor.proto"; -import "common/describable/v1/describable.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/pagination/v1/cursor.proto"; +import "api/proto/common/describable/v1/describable.proto"; // Supported blockchain networks for the managed wallets. enum ChainNetwork { diff --git a/api/proto/gateway/mntx/v1/mntx.proto b/api/proto/gateway/mntx/v1/mntx.proto index 834628c9..23e8fb88 100644 --- a/api/proto/gateway/mntx/v1/mntx.proto +++ b/api/proto/gateway/mntx/v1/mntx.proto @@ -5,7 +5,7 @@ package mntx.gateway.v1; option go_package = "github.com/tech/sendico/pkg/proto/gateway/mntx/v1;mntxv1"; import "google/protobuf/timestamp.proto"; -import "common/gateway/v1/gateway.proto"; +import "api/proto/common/gateway/v1/gateway.proto"; // Lifecycle status of a payout handled by Monetix. enum PayoutStatus { diff --git a/api/proto/ledger/v1/ledger.proto b/api/proto/ledger/v1/ledger.proto index 957b12bd..486bb084 100644 --- a/api/proto/ledger/v1/ledger.proto +++ b/api/proto/ledger/v1/ledger.proto @@ -6,8 +6,8 @@ option go_package = "github.com/tech/sendico/pkg/proto/ledger/v1;ledgerv1"; import "google/protobuf/timestamp.proto"; import "google/protobuf/wrappers.proto"; -import "common/describable/v1/describable.proto"; -import "common/money/v1/money.proto"; +import "api/proto/common/describable/v1/describable.proto"; +import "api/proto/common/money/v1/money.proto"; // ===== Enums ===== diff --git a/api/proto/oracle/v1/oracle.proto b/api/proto/oracle/v1/oracle.proto index eeff33cd..4e8675a3 100644 --- a/api/proto/oracle/v1/oracle.proto +++ b/api/proto/oracle/v1/oracle.proto @@ -4,9 +4,11 @@ package oracle.v1; option go_package = "github.com/tech/sendico/pkg/proto/oracle/v1;oraclev1"; -import "common/money/v1/money.proto"; -import "common/fx/v1/fx.proto"; -import "common/trace/v1/trace.proto"; +import "google/protobuf/timestamp.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/fx/v1/fx.proto"; +import "api/proto/common/trace/v1/trace.proto"; + message RateSnapshot { common.fx.v1.CurrencyPair pair = 1; @@ -46,6 +48,7 @@ message Quote { string provider = 8; string rate_ref = 9; bool firm = 10; + google.protobuf.Timestamp priced_at = 11; } message GetQuoteRequest { diff --git a/api/proto/payments/endpoint/v1/endpoint.proto b/api/proto/payments/endpoint/v1/endpoint.proto new file mode 100644 index 00000000..de1bd114 --- /dev/null +++ b/api/proto/payments/endpoint/v1/endpoint.proto @@ -0,0 +1,40 @@ +syntax = "proto3"; + +package payments.endpoint.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/payments/endpoint/v1;endpointv1"; + +import "api/proto/common/describable/v1/describable.proto"; +import "api/proto/common/permission_bound/v1/pbound.proto"; + +enum PaymentMethodType { + PAYMENT_METHOD_TYPE_UNSPECIFIED = 0; + PAYMENT_METHOD_TYPE_IBAN = 1; + PAYMENT_METHOD_TYPE_CARD = 2; + PAYMENT_METHOD_TYPE_CARD_TOKEN = 3; + PAYMENT_METHOD_TYPE_BANK_ACCOUNT = 4; + PAYMENT_METHOD_TYPE_WALLET = 5; + PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS = 6; + PAYMENT_METHOD_TYPE_LEDGER = 7; +} + +message PaymentMethod { + common.describable.v1.Describable describable = 1; + string recipient_ref = 2; + PaymentMethodType type = 3; + bytes data = 4; + bool is_main = 5; +} + +message PaymentEndpoint { + oneof source { + string payment_method_ref = 1; + PaymentMethod payment_method = 2; + string payee_ref = 3; + } +} + +message PaymentMethodRecord { + common.pbound.v1.PermissionBound permission_bound = 1; + PaymentMethod payment_method = 2; +} diff --git a/api/proto/payments/methods/v1/methods.proto b/api/proto/payments/methods/v1/methods.proto index 37def79f..b2a4c964 100644 --- a/api/proto/payments/methods/v1/methods.proto +++ b/api/proto/payments/methods/v1/methods.proto @@ -4,18 +4,17 @@ package payments.methods.v1; option go_package = "github.com/tech/sendico/pkg/proto/payments/methods/v1;methodsv1"; -import "google/protobuf/wrappers.proto"; - -import "common/pagination/v2/cursor.proto"; +import "api/proto/common/pagination/v2/cursor.proto"; +import "api/proto/payments/endpoint/v1/endpoint.proto"; message CreatePaymentMethodRequest { string account_ref = 1; string organization_ref = 2; - bytes payment_method_json = 3; + payments.endpoint.v1.PaymentMethod payment_method = 3; } message CreatePaymentMethodResponse { - bytes payment_method_json = 1; + payments.endpoint.v1.PaymentMethodRecord payment_method_record = 1; } message GetPaymentMethodRequest { @@ -24,16 +23,16 @@ message GetPaymentMethodRequest { } message GetPaymentMethodResponse { - bytes payment_method_json = 1; + payments.endpoint.v1.PaymentMethodRecord payment_method_record = 1; } message UpdatePaymentMethodRequest { string account_ref = 1; - bytes payment_method_json = 2; + payments.endpoint.v1.PaymentMethodRecord payment_method_record = 2; } message UpdatePaymentMethodResponse { - bytes payment_method_json = 1; + payments.endpoint.v1.PaymentMethodRecord payment_method_record = 1; } message DeletePaymentMethodRequest { @@ -62,14 +61,21 @@ message ListPaymentMethodsRequest { } message ListPaymentMethodsResponse { - repeated bytes payment_methods_json = 1; + repeated payments.endpoint.v1.PaymentMethodRecord payment_methods = 1; } +// PaymentMethodsService provides operations for managing payment methods. service PaymentMethodsService { + // CreatePaymentMethod creates a new payment method. rpc CreatePaymentMethod(CreatePaymentMethodRequest) returns (CreatePaymentMethodResponse); + // GetPaymentMethod retrieves a payment method by reference. rpc GetPaymentMethod(GetPaymentMethodRequest) returns (GetPaymentMethodResponse); + // UpdatePaymentMethod updates an existing payment method. rpc UpdatePaymentMethod(UpdatePaymentMethodRequest) returns (UpdatePaymentMethodResponse); + // Delete exising payment method rpc DeletePaymentMethod(DeletePaymentMethodRequest) returns (DeletePaymentMethodResponse); + // SetPaymentMethodArchived sets the archived status of a payment method. rpc SetPaymentMethodArchived(SetPaymentMethodArchivedRequest) returns (SetPaymentMethodArchivedResponse); + // ListPaymentMethods retrieves a list of payment methods. rpc ListPaymentMethods(ListPaymentMethodsRequest) returns (ListPaymentMethodsResponse); } diff --git a/api/proto/payments/orchestration/v1/orchestration.proto b/api/proto/payments/orchestration/v1/orchestration.proto index 3aba9ceb..59da61d8 100644 --- a/api/proto/payments/orchestration/v1/orchestration.proto +++ b/api/proto/payments/orchestration/v1/orchestration.proto @@ -4,11 +4,11 @@ package payments.orchestration.v1; option go_package = "github.com/tech/sendico/pkg/proto/payments/orchestration/v1;orchestrationv1"; -import "common/pagination/v1/cursor.proto"; -import "billing/fees/v1/fees.proto"; -import "gateway/chain/v1/chain.proto"; -import "gateway/mntx/v1/mntx.proto"; -import "payments/shared/v1/shared.proto"; +import "api/proto/common/pagination/v1/cursor.proto"; +import "api/proto/billing/fees/v1/fees.proto"; +import "api/proto/gateway/chain/v1/chain.proto"; +import "api/proto/gateway/mntx/v1/mntx.proto"; +import "api/proto/payments/shared/v1/shared.proto"; message InitiatePaymentsRequest { payments.shared.v1.RequestMeta meta = 1; diff --git a/api/proto/payments/payment/v1/payment.proto b/api/proto/payments/payment/v1/payment.proto new file mode 100644 index 00000000..01c08216 --- /dev/null +++ b/api/proto/payments/payment/v1/payment.proto @@ -0,0 +1,20 @@ +syntax = "proto3"; + +package payments.payment.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/payments/payment/v1;paymentv1"; + +import "api/proto/payments/transfer/v1/transfer.proto"; + + +// ------------------------- +// External payment semantics +// ------------------------- +message PaymentIntent { + payments.transfer.v1.TransferIntent transfer = 1; + + string payer_ref = 2; + string payee_ref = 3; + + string purpose = 4; +} diff --git a/api/proto/payments/quotation/v1/quotation.proto b/api/proto/payments/quotation/v1/quotation.proto index 093eebc0..bc53f351 100644 --- a/api/proto/payments/quotation/v1/quotation.proto +++ b/api/proto/payments/quotation/v1/quotation.proto @@ -4,7 +4,8 @@ package payments.quotation.v1; option go_package = "github.com/tech/sendico/pkg/proto/payments/quotation/v1;quotationv1"; -import "payments/shared/v1/shared.proto"; +import "api/proto/payments/shared/v1/shared.proto"; + message QuotePaymentRequest { payments.shared.v1.RequestMeta meta = 1; @@ -35,6 +36,8 @@ message QuotePaymentsResponse { } service QuotationService { + // QuotePayment returns a quote for a single payment request. rpc QuotePayment(QuotePaymentRequest) returns (QuotePaymentResponse); + // QuotePayments returns quotes for multiple payment requests. rpc QuotePayments(QuotePaymentsRequest) returns (QuotePaymentsResponse); } diff --git a/api/proto/payments/quotation/v2/interface.proto b/api/proto/payments/quotation/v2/interface.proto new file mode 100644 index 00000000..d93f9499 --- /dev/null +++ b/api/proto/payments/quotation/v2/interface.proto @@ -0,0 +1,59 @@ +syntax = "proto3"; + +package payments.quotation.v2; + +option go_package = "github.com/tech/sendico/pkg/proto/payments/quotation/v2;quotationv2"; + +import "google/protobuf/timestamp.proto"; +import "api/proto/common/storable/v1/storable.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/fx/v1/fx.proto"; +import "api/proto/billing/fees/v1/fees.proto"; +import "api/proto/oracle/v1/oracle.proto"; + +enum QuoteKind { + QUOTE_KIND_UNSPECIFIED = 0; + + QUOTE_KIND_EXECUTABLE = 1; // can be executed now (subject to execution-time checks) + QUOTE_KIND_INDICATIVE = 2; // informational only +} + +enum QuoteState { + QUOTE_STATE_UNSPECIFIED = 0; + + QUOTE_STATE_ACTIVE = 1; + QUOTE_STATE_EXPIRED = 2; +} + +enum QuoteBlockReason { + QUOTE_BLOCK_REASON_UNSPECIFIED = 0; + + QUOTE_BLOCK_REASON_ROUTE_UNAVAILABLE = 1; + QUOTE_BLOCK_REASON_LIMIT_BLOCKED = 2; + QUOTE_BLOCK_REASON_RISK_BLOCKED = 3; + QUOTE_BLOCK_REASON_INSUFFICIENT_LIQUIDITY = 4; + QUOTE_BLOCK_REASON_PRICE_STALE = 5; + QUOTE_BLOCK_REASON_AMOUNT_TOO_SMALL = 6; + QUOTE_BLOCK_REASON_AMOUNT_TOO_LARGE = 7; +} + + +message PaymentQuote { + common.storable.v1.Storable storable = 1; + QuoteKind kind = 2; + QuoteState state = 3; + optional QuoteBlockReason block_reason = 4; + + common.money.v1.Money debit_amount = 5; + common.money.v1.Money credit_amount = 6; + + repeated fees.v1.DerivedPostingLine fee_lines = 7; + repeated fees.v1.AppliedRule fee_rules = 8; + + oracle.v1.Quote fx_quote = 9; + + string quote_ref = 10; + + google.protobuf.Timestamp expires_at = 11; + google.protobuf.Timestamp priced_at = 12; +} diff --git a/api/proto/payments/quotation/v2/quotation.proto b/api/proto/payments/quotation/v2/quotation.proto new file mode 100644 index 00000000..d3580429 --- /dev/null +++ b/api/proto/payments/quotation/v2/quotation.proto @@ -0,0 +1,37 @@ +syntax = "proto3"; + +package payments.quotation.v2; + +option go_package = "github.com/tech/sendico/pkg/proto/payments/quotation/v2;quotationv2"; + +import "api/proto/payments/shared/v1/shared.proto"; +import "api/proto/payments/transfer/v1/transfer.proto"; +import "api/proto/payments/quotation/v2/interface.proto"; + + +message QuotePaymentRequest { + payments.shared.v1.RequestMeta meta = 1; + string idempotency_key = 2; + payments.transfer.v1.TransferIntent intent = 3; + bool preview_only = 4; + string initiator_ref = 5; +} + +message QuotePaymentResponse { + payments.quotation.v2.PaymentQuote quote = 1; + string idempotency_key = 2; +} + +message QuotePaymentsRequest { + payments.shared.v1.RequestMeta meta = 1; + string idempotency_key = 2; + repeated payments.transfer.v1.TransferIntent intents = 3; + bool preview_only = 4; + string initiator_ref = 5; +} + +message QuotePaymentsResponse { + string quote_ref = 1; + repeated payments.quotation.v2.PaymentQuote quotes = 3; + string idempotency_key = 4; +} diff --git a/api/proto/payments/shared/v1/shared.proto b/api/proto/payments/shared/v1/shared.proto index e14d0e4f..cc1a5a77 100644 --- a/api/proto/payments/shared/v1/shared.proto +++ b/api/proto/payments/shared/v1/shared.proto @@ -5,13 +5,14 @@ package payments.shared.v1; option go_package = "github.com/tech/sendico/pkg/proto/payments/shared/v1;sharedv1"; import "google/protobuf/timestamp.proto"; -import "common/money/v1/money.proto"; -import "common/fx/v1/fx.proto"; -import "common/gateway/v1/gateway.proto"; -import "common/trace/v1/trace.proto"; -import "billing/fees/v1/fees.proto"; -import "gateway/chain/v1/chain.proto"; -import "oracle/v1/oracle.proto"; +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/fx/v1/fx.proto"; +import "api/proto/common/gateway/v1/gateway.proto"; +import "api/proto/common/payment/v1/settlement.proto"; +import "api/proto/common/trace/v1/trace.proto"; +import "api/proto/billing/fees/v1/fees.proto"; +import "api/proto/gateway/chain/v1/chain.proto"; +import "api/proto/oracle/v1/oracle.proto"; enum PaymentKind { PAYMENT_KIND_UNSPECIFIED = 0; @@ -20,13 +21,6 @@ enum PaymentKind { PAYMENT_KIND_FX_CONVERSION = 3; } -// SettlementMode defines how to treat fees/FX variance for payouts. -enum SettlementMode { - SETTLEMENT_UNSPECIFIED = 0; - SETTLEMENT_FIX_SOURCE = 1; // customer pays fees; sent amount fixed - SETTLEMENT_FIX_RECEIVED = 2; // receiver gets fixed amount; source flexes -} - enum PaymentState { PAYMENT_STATE_UNSPECIFIED = 0; PAYMENT_STATE_ACCEPTED = 1; @@ -111,7 +105,7 @@ message PaymentIntent { FXIntent fx = 6; fees.v1.PolicyOverrides fee_policy = 7; map attributes = 8; - SettlementMode settlement_mode = 9; + common.payment.v1.SettlementMode settlement_mode = 9; Customer customer = 10; string settlement_currency = 11; string ref = 12; diff --git a/api/proto/payments/transfer/v1/transfer.proto b/api/proto/payments/transfer/v1/transfer.proto new file mode 100644 index 00000000..d22314ea --- /dev/null +++ b/api/proto/payments/transfer/v1/transfer.proto @@ -0,0 +1,21 @@ +syntax = "proto3"; + +package payments.transfer.v1; + +option go_package = "github.com/tech/sendico/pkg/proto/payments/transfer/v1;transferv1"; + +import "api/proto/common/money/v1/money.proto"; +import "api/proto/common/storable/v1/storable.proto"; +import "api/proto/payments/endpoint/v1/endpoint.proto"; + + +// ------------------------- +// Base value movement +// ------------------------- +message TransferIntent { + payments.endpoint.v1.PaymentEndpoint source = 1; + payments.endpoint.v1.PaymentEndpoint destination = 2; + common.money.v1.Money amount = 3; + + string comment = 4; +} diff --git a/api/server/go.mod b/api/server/go.mod index 4a5ab6bd..2c0df783 100644 --- a/api/server/go.mod +++ b/api/server/go.mod @@ -37,7 +37,7 @@ require ( go.mongodb.org/mongo-driver/v2 v2.5.0 go.uber.org/zap v1.27.1 golang.org/x/net v0.50.0 - google.golang.org/grpc v1.79.0 + google.golang.org/grpc v1.79.1 google.golang.org/protobuf v1.36.11 gopkg.in/yaml.v3 v3.0.1 moul.io/chizap v1.0.3 diff --git a/api/server/go.sum b/api/server/go.sum index eb538f72..309e0e39 100644 --- a/api/server/go.sum +++ b/api/server/go.sum @@ -365,8 +365,8 @@ google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57 h1: google.golang.org/genproto/googleapis/api v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:kSJwQxqmFXeo79zOmbrALdflXQeAYcUbgS7PbpMknCY= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57 h1:mWPCjDEyshlQYzBpMNHaEof6UX1PmHcaUODUywQ0uac= google.golang.org/genproto/googleapis/rpc v0.0.0-20260209200024-4cfbd4190f57/go.mod h1:j9x/tPzZkyxcgEFkiKEEGxfvyumM01BEtsW8xzOahRQ= -google.golang.org/grpc v1.79.0 h1:6/+EFlxsMyoSbHbBoEDx94n/Ycx/bi0IhJ5Qh7b7LaA= -google.golang.org/grpc v1.79.0/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= +google.golang.org/grpc v1.79.1 h1:zGhSi45ODB9/p3VAawt9a+O/MULLl9dpizzNNpq7flY= +google.golang.org/grpc v1.79.1/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ= google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE= google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= diff --git a/api/server/interface/api/sresponse/payment.go b/api/server/interface/api/sresponse/payment.go index 6c0ed8a6..31f36674 100644 --- a/api/server/interface/api/sresponse/payment.go +++ b/api/server/interface/api/sresponse/payment.go @@ -32,6 +32,7 @@ type FxQuote struct { BaseAmount *paymenttypes.Money `json:"baseAmount,omitempty"` QuoteAmount *paymenttypes.Money `json:"quoteAmount,omitempty"` ExpiresAtUnixMs int64 `json:"expiresAtUnixMs,omitempty"` + PricedAtUnixMs int64 `json:"pricedAtUnixMs,omitempty"` Provider string `json:"provider,omitempty"` RateRef string `json:"rateRef,omitempty"` Firm bool `json:"firm,omitempty"` @@ -163,6 +164,10 @@ func toFxQuote(q *oraclev1.Quote) *FxQuote { return nil } pair := q.GetPair() + pricedAtUnixMs := int64(0) + if ts := q.GetPricedAt(); ts != nil { + pricedAtUnixMs = ts.AsTime().UnixMilli() + } base := "" quote := "" if pair != nil { @@ -178,6 +183,7 @@ func toFxQuote(q *oraclev1.Quote) *FxQuote { BaseAmount: toMoney(q.GetBaseAmount()), QuoteAmount: toMoney(q.GetQuoteAmount()), ExpiresAtUnixMs: q.GetExpiresAtUnixMs(), + PricedAtUnixMs: pricedAtUnixMs, Provider: q.GetProvider(), RateRef: q.GetRateRef(), Firm: q.GetFirm(), diff --git a/api/server/internal/server/paymethodsimp/service.go b/api/server/internal/server/paymethodsimp/service.go index c7763ea4..1a4abd12 100644 --- a/api/server/internal/server/paymethodsimp/service.go +++ b/api/server/internal/server/paymethodsimp/service.go @@ -3,6 +3,7 @@ package paymethodsimp import ( "context" "encoding/json" + "fmt" "io" "net/http" "os" @@ -16,13 +17,21 @@ import ( "github.com/tech/sendico/pkg/mlogger" "github.com/tech/sendico/pkg/model" "github.com/tech/sendico/pkg/mservice" + archivablev1 "github.com/tech/sendico/pkg/proto/common/archivable/v1" + describablev1 "github.com/tech/sendico/pkg/proto/common/describable/v1" + oboundv1 "github.com/tech/sendico/pkg/proto/common/organization_bound/v1" paginationv2 "github.com/tech/sendico/pkg/proto/common/pagination/v2" + pboundv1 "github.com/tech/sendico/pkg/proto/common/permission_bound/v1" + storablev1 "github.com/tech/sendico/pkg/proto/common/storable/v1" + endpointv1 "github.com/tech/sendico/pkg/proto/payments/endpoint/v1" methodsv1 "github.com/tech/sendico/pkg/proto/payments/methods/v1" eapi "github.com/tech/sendico/server/interface/api" "github.com/tech/sendico/server/interface/api/sresponse" mutil "github.com/tech/sendico/server/internal/mutil/param" + "go.mongodb.org/mongo-driver/v2/bson" "google.golang.org/grpc/codes" "google.golang.org/grpc/status" + "google.golang.org/protobuf/types/known/timestamppb" "google.golang.org/protobuf/types/known/wrapperspb" ) @@ -98,17 +107,25 @@ func (a *PaymentMethodsAPI) create(r *http.Request, account *model.Account, toke if err != nil { return response.BadPayload(a.logger, a.Name(), err) } + pm, err := decodePaymentMethodJSON(payload) + if err != nil { + return response.BadPayload(a.logger, a.Name(), err) + } + method, err := encodePaymentMethodProto(pm) + if err != nil { + return response.Internal(a.logger, a.Name(), err) + } resp, err := a.client.CreatePaymentMethod(r.Context(), &methodsv1.CreatePaymentMethodRequest{ - AccountRef: account.ID.Hex(), - OrganizationRef: orgRef.Hex(), - PaymentMethodJson: payload, + AccountRef: account.ID.Hex(), + OrganizationRef: orgRef.Hex(), + PaymentMethod: method, }) if err != nil { return grpcErrorResponse(a.logger, a.Name(), err) } - pm, err := decodePaymentMethod(resp.GetPaymentMethodJson()) + pm, err = decodePaymentMethodRecord(resp.GetPaymentMethodRecord()) if err != nil { return response.Internal(a.logger, a.Name(), err) } @@ -140,7 +157,7 @@ func (a *PaymentMethodsAPI) list(r *http.Request, account *model.Account, token return grpcErrorResponse(a.logger, a.Name(), err) } - items, err := decodePaymentMethods(resp.GetPaymentMethodsJson()) + items, err := decodePaymentMethods(resp.GetPaymentMethods()) if err != nil { return response.Internal(a.logger, a.Name(), err) } @@ -161,7 +178,7 @@ func (a *PaymentMethodsAPI) get(r *http.Request, account *model.Account, token * return grpcErrorResponse(a.logger, a.Name(), err) } - pm, err := decodePaymentMethod(resp.GetPaymentMethodJson()) + pm, err := decodePaymentMethodRecord(resp.GetPaymentMethodRecord()) if err != nil { return response.Internal(a.logger, a.Name(), err) } @@ -173,16 +190,24 @@ func (a *PaymentMethodsAPI) update(r *http.Request, account *model.Account, toke if err != nil { return response.BadPayload(a.logger, a.Name(), err) } + pm, err := decodePaymentMethodJSON(payload) + if err != nil { + return response.BadPayload(a.logger, a.Name(), err) + } + record, err := encodePaymentMethodRecord(pm) + if err != nil { + return response.Internal(a.logger, a.Name(), err) + } resp, err := a.client.UpdatePaymentMethod(r.Context(), &methodsv1.UpdatePaymentMethodRequest{ - AccountRef: account.ID.Hex(), - PaymentMethodJson: payload, + AccountRef: account.ID.Hex(), + PaymentMethodRecord: record, }) if err != nil { return grpcErrorResponse(a.logger, a.Name(), err) } - pm, err := decodePaymentMethod(resp.GetPaymentMethodJson()) + pm, err = decodePaymentMethodRecord(resp.GetPaymentMethodRecord()) if err != nil { return response.Internal(a.logger, a.Name(), err) } @@ -300,7 +325,7 @@ func toProtoCursor(cursor *model.ViewCursor) *paginationv2.ViewCursor { return res } -func decodePaymentMethod(payload []byte) (*model.PaymentMethod, error) { +func decodePaymentMethodJSON(payload []byte) (*model.PaymentMethod, error) { var pm model.PaymentMethod if err := json.Unmarshal(payload, &pm); err != nil { return nil, err @@ -308,13 +333,78 @@ func decodePaymentMethod(payload []byte) (*model.PaymentMethod, error) { return &pm, nil } -func decodePaymentMethods(items [][]byte) ([]model.PaymentMethod, error) { +func decodePaymentMethodRecord(record *endpointv1.PaymentMethodRecord) (*model.PaymentMethod, error) { + if record == nil { + return nil, merrors.InvalidArgument("payment_method_record is required") + } + pm, err := decodePaymentMethodProto(record.GetPaymentMethod()) + if err != nil { + return nil, err + } + if err := applyPermissionBound(pm, record.GetPermissionBound()); err != nil { + return nil, err + } + return pm, nil +} + +func decodePaymentMethodProto(method *endpointv1.PaymentMethod) (*model.PaymentMethod, error) { + if method == nil { + return nil, merrors.InvalidArgument("payment_method is required") + } + + recipientRef, err := parseRequiredObjectID(method.GetRecipientRef(), "payment_method.recipient_ref") + if err != nil { + return nil, err + } + pt, err := paymentTypeFromProto(method.GetType(), "payment_method.type") + if err != nil { + return nil, err + } + + return &model.PaymentMethod{ + Describable: describableFromProto(method.GetDescribable()), + RecipientRef: recipientRef, + Type: pt, + Data: cloneBytes(method.GetData()), + IsMain: method.GetIsMain(), + }, nil +} + +func encodePaymentMethodProto(pm *model.PaymentMethod) (*endpointv1.PaymentMethod, error) { + if pm == nil { + return nil, merrors.InvalidArgument("payment method is required") + } + pt, err := paymentTypeToProto(pm.Type) + if err != nil { + return nil, err + } + return &endpointv1.PaymentMethod{ + Describable: describableToProto(pm.Describable), + RecipientRef: toObjectHex(pm.RecipientRef), + Type: pt, + Data: cloneBytes(pm.Data), + IsMain: pm.IsMain, + }, nil +} + +func encodePaymentMethodRecord(pm *model.PaymentMethod) (*endpointv1.PaymentMethodRecord, error) { + method, err := encodePaymentMethodProto(pm) + if err != nil { + return nil, err + } + return &endpointv1.PaymentMethodRecord{ + PermissionBound: permissionBoundFromModel(pm), + PaymentMethod: method, + }, nil +} + +func decodePaymentMethods(items []*endpointv1.PaymentMethodRecord) ([]model.PaymentMethod, error) { if len(items) == 0 { return nil, nil } res := make([]model.PaymentMethod, 0, len(items)) for i := range items { - pm, err := decodePaymentMethod(items[i]) + pm, err := decodePaymentMethodRecord(items[i]) if err != nil { return nil, err } @@ -323,6 +413,183 @@ func decodePaymentMethods(items [][]byte) ([]model.PaymentMethod, error) { return res, nil } +func paymentTypeFromProto(value endpointv1.PaymentMethodType, field string) (model.PaymentType, error) { + switch value { + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN: + return model.PaymentTypeIban, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD: + return model.PaymentTypeCard, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN: + return model.PaymentTypeCardToken, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT: + return model.PaymentTypeBankAccount, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET: + return model.PaymentTypeWallet, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS: + return model.PaymentTypeCryptoAddress, nil + case endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER: + return model.PaymentTypeLedger, nil + default: + return model.PaymentTypeIban, merrors.InvalidArgument(fmt.Sprintf("%s has unsupported value: %s", field, value.String()), field) + } +} + +func paymentTypeToProto(value model.PaymentType) (endpointv1.PaymentMethodType, error) { + switch value { + case model.PaymentTypeIban: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_IBAN, nil + case model.PaymentTypeCard: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD, nil + case model.PaymentTypeCardToken: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CARD_TOKEN, nil + case model.PaymentTypeBankAccount: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_BANK_ACCOUNT, nil + case model.PaymentTypeWallet: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_WALLET, nil + case model.PaymentTypeCryptoAddress: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_CRYPTO_ADDRESS, nil + case model.PaymentTypeLedger: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_LEDGER, nil + default: + return endpointv1.PaymentMethodType_PAYMENT_METHOD_TYPE_UNSPECIFIED, merrors.InvalidArgument(fmt.Sprintf("unsupported payment method type: %s", value.String()), "type") + } +} + +func describableFromProto(src *describablev1.Describable) model.Describable { + if src == nil { + return model.Describable{} + } + res := model.Describable{Name: src.GetName()} + if src.Description != nil { + v := src.GetDescription() + res.Description = &v + } + return res +} + +func describableToProto(src model.Describable) *describablev1.Describable { + if strings.TrimSpace(src.Name) == "" && src.Description == nil { + return nil + } + res := &describablev1.Describable{ + Name: src.Name, + } + if src.Description != nil { + v := *src.Description + res.Description = &v + } + return res +} + +func cloneBytes(src []byte) []byte { + if len(src) == 0 { + return nil + } + dst := make([]byte, len(src)) + copy(dst, src) + return dst +} + +func permissionBoundFromModel(pm *model.PaymentMethod) *pboundv1.PermissionBound { + if pm == nil { + return nil + } + return &pboundv1.PermissionBound{ + Storable: &storablev1.Storable{ + Id: toObjectHex(pm.ID), + CreatedAt: toProtoTime(pm.CreatedAt), + UpdatedAt: toProtoTime(pm.UpdatedAt), + }, + Archivable: &archivablev1.Archivable{ + IsArchived: pm.Archived, + }, + OrganizationBound: &oboundv1.OrganizationBound{ + OrganizationRef: toObjectHex(pm.GetOrganizationRef()), + }, + PermissionRef: toObjectHex(pm.GetPermissionRef()), + } +} + +func applyPermissionBound(pm *model.PaymentMethod, src *pboundv1.PermissionBound) error { + if pm == nil || src == nil { + return nil + } + + if storable := src.GetStorable(); storable != nil { + if methodRef, err := parseOptionalObjectID(storable.GetId(), "payment_method_record.permission_bound.storable.id"); err != nil { + return err + } else if methodRef != bson.NilObjectID { + pm.ID = methodRef + } + pm.CreatedAt = fromProtoTime(storable.GetCreatedAt()) + pm.UpdatedAt = fromProtoTime(storable.GetUpdatedAt()) + } + + if archivable := src.GetArchivable(); archivable != nil { + pm.Archived = archivable.GetIsArchived() + } + + if orgBound := src.GetOrganizationBound(); orgBound != nil { + if orgRef, err := parseOptionalObjectID(orgBound.GetOrganizationRef(), "payment_method_record.permission_bound.organization_bound.organization_ref"); err != nil { + return err + } else if orgRef != bson.NilObjectID { + pm.SetOrganizationRef(orgRef) + } + } + + if permissionRef, err := parseOptionalObjectID(src.GetPermissionRef(), "payment_method_record.permission_bound.permission_ref"); err != nil { + return err + } else if permissionRef != bson.NilObjectID { + pm.SetPermissionRef(permissionRef) + } + + return nil +} + +func parseOptionalObjectID(value, field string) (bson.ObjectID, error) { + trimmed := strings.TrimSpace(value) + if trimmed == "" { + return bson.NilObjectID, nil + } + ref, err := bson.ObjectIDFromHex(trimmed) + if err != nil { + return bson.NilObjectID, merrors.InvalidArgument(fmt.Sprintf("%s must be a valid object id", field), field) + } + return ref, nil +} + +func parseRequiredObjectID(value, field string) (bson.ObjectID, error) { + ref, err := parseOptionalObjectID(value, field) + if err != nil { + return bson.NilObjectID, err + } + if ref == bson.NilObjectID { + return bson.NilObjectID, merrors.InvalidArgument(field+" is required", field) + } + return ref, nil +} + +func toObjectHex(value bson.ObjectID) string { + if value == bson.NilObjectID { + return "" + } + return value.Hex() +} + +func toProtoTime(value time.Time) *timestamppb.Timestamp { + if value.IsZero() { + return nil + } + return timestamppb.New(value) +} + +func fromProtoTime(value *timestamppb.Timestamp) time.Time { + if value == nil { + return time.Time{} + } + return value.AsTime() +} + func grpcErrorResponse(logger mlogger.Logger, source mservice.Type, err error) http.HandlerFunc { statusErr, ok := status.FromError(err) if !ok { diff --git a/ci/scripts/proto/generate.sh b/ci/scripts/proto/generate.sh index 3c7aa9b4..0228756a 100755 --- a/ci/scripts/proto/generate.sh +++ b/ci/scripts/proto/generate.sh @@ -36,7 +36,13 @@ PROTOC_ARGS="" if [ -n "${PROTOC_INCLUDE}" ]; then PROTOC_ARGS="-I=${PROTOC_INCLUDE}" fi -PROTOC_ARGS="${PROTOC_ARGS} -I=${PROTO_DIR}" +PROTOC_ARGS="${PROTOC_ARGS} -I=.. -I=${PROTO_DIR}" + +PROTOC_ARGS_MESSAGING="" +if [ -n "${PROTOC_INCLUDE}" ]; then + PROTOC_ARGS_MESSAGING="-I=${PROTOC_INCLUDE}" +fi +PROTOC_ARGS_MESSAGING="${PROTOC_ARGS_MESSAGING} -I=${PROTO_DIR}" generate_go() { # shellcheck disable=SC2086 @@ -47,6 +53,15 @@ generate_go() { "$@" } +generate_go_messaging() { + # shellcheck disable=SC2086 + "${PROTOC_BIN}" \ + ${PROTOC_ARGS_MESSAGING} \ + --go_out="${GO_OUT_DIR}" \ + --go_opt=module="${GO_MODULE}" \ + "$@" +} + generate_go_with_grpc() { # shellcheck disable=SC2086 "${PROTOC_BIN}" \ @@ -76,15 +91,15 @@ list_protos() { cd "${API_DIR}" -messaging_files="$(list_protos "${PROTO_DIR}" 1)" +messaging_files="$(find "${PROTO_DIR}" -maxdepth 1 -type f -name '*.proto' -exec basename {} \; | sort)" if [ -n "${messaging_files}" ]; then info "Compiling messaging protos" rm -rf ./pkg/generated/gmessaging ./pkg/messaging/internal/generated set -- $messaging_files - generate_go "$@" + generate_go_messaging "$@" fi -common_files="$(list_protos "${PROTO_DIR}/common")" +common_files="$(list_protos "${PROTO_DIR}/common" | sed 's#^\./##' | sed 's#^proto/#api/proto/#')" if [ -n "${common_files}" ]; then info "Compiling common shared protos" clean_pb_files "./pkg/proto" @@ -140,6 +155,12 @@ if [ -f "${PROTO_DIR}/payments/quotation/v1/quotation.proto" ]; then generate_go_with_grpc "${PROTO_DIR}/payments/quotation/v1/quotation.proto" fi +if [ -f "${PROTO_DIR}/payments/endpoint/v1/endpoint.proto" ]; then + info "Compiling payments endpoint protos" + clean_pb_files "./pkg/proto/payments/endpoint" + generate_go "${PROTO_DIR}/payments/endpoint/v1/endpoint.proto" +fi + if [ -f "${PROTO_DIR}/payments/methods/v1/methods.proto" ]; then info "Compiling payments methods protos" clean_pb_files "./pkg/proto/payments/methods" -- 2.49.1 From 01020bb694acc023c15d0b82d226b57a1dcc4dcd Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 15:41:42 +0100 Subject: [PATCH 18/21] fixed settlement mode import --- .../internal/service/orchestrator/convert.go | 15 ++++++++------- .../internal/service/orchestrator/helpers.go | 7 ++++--- .../internal/service/orchestrator/helpers_test.go | 6 +++--- .../service/orchestrator/service_helpers_test.go | 3 ++- .../internal/service/quotation/convert.go | 15 ++++++++------- .../internal/service/quotation/helpers.go | 8 ++++---- .../internal/server/paymentapiimp/mapper.go | 11 ++++++----- 7 files changed, 35 insertions(+), 30 deletions(-) diff --git a/api/payments/orchestrator/internal/service/orchestrator/convert.go b/api/payments/orchestrator/internal/service/orchestrator/convert.go index 3516d077..12e6b3ce 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/convert.go +++ b/api/payments/orchestrator/internal/service/orchestrator/convert.go @@ -12,6 +12,7 @@ import ( fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" gatewayv1 "github.com/tech/sendico/pkg/proto/common/gateway/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1" @@ -553,25 +554,25 @@ func protoFailureFromModel(code model.PaymentFailureCode) sharedv1.PaymentFailur } } -func settlementModeFromProto(mode sharedv1.SettlementMode) model.SettlementMode { +func settlementModeFromProto(mode paymentv1.SettlementMode) model.SettlementMode { switch mode { - case sharedv1.SettlementMode_SETTLEMENT_FIX_SOURCE: + case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE: return model.SettlementModeFixSource - case sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: + case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: return model.SettlementModeFixReceived default: return model.SettlementModeUnspecified } } -func settlementModeToProto(mode model.SettlementMode) sharedv1.SettlementMode { +func settlementModeToProto(mode model.SettlementMode) paymentv1.SettlementMode { switch mode { case model.SettlementModeFixSource: - return sharedv1.SettlementMode_SETTLEMENT_FIX_SOURCE + return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE case model.SettlementModeFixReceived: - return sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED + return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED default: - return sharedv1.SettlementMode_SETTLEMENT_UNSPECIFIED + return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED } } diff --git a/api/payments/orchestrator/internal/service/orchestrator/helpers.go b/api/payments/orchestrator/internal/service/orchestrator/helpers.go index da75347f..28717bff 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/helpers.go +++ b/api/payments/orchestrator/internal/service/orchestrator/helpers.go @@ -14,6 +14,7 @@ import ( 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" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" ) type moneyGetter interface { @@ -100,7 +101,7 @@ func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, s } } -func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote, mode sharedv1.SettlementMode) (*moneyv1.Money, *moneyv1.Money) { +func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote, mode paymentv1.SettlementMode) (*moneyv1.Money, *moneyv1.Money) { if pay == nil { return nil, nil } @@ -142,7 +143,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est } switch mode { - case sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: + case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: // Sender pays the fee: keep settlement fixed, increase debit. applyChargeToDebit(fee) default: @@ -152,7 +153,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est if network != nil && network.GetNetworkFee() != nil { switch mode { - case sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: + case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: applyChargeToDebit(network.GetNetworkFee()) default: applyChargeToSettlement(network.GetNetworkFee()) diff --git a/api/payments/orchestrator/internal/service/orchestrator/helpers_test.go b/api/payments/orchestrator/internal/service/orchestrator/helpers_test.go index 1f86bcff..cb49ab5a 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/helpers_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/helpers_test.go @@ -7,9 +7,9 @@ import ( 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" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" - sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" ) func TestResolveTradeAmountsBuyBase(t *testing.T) { @@ -50,7 +50,7 @@ func TestComputeAggregatesConvertsCurrencies(t *testing.T) { }, } - debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote, sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED) + debit, settlement := computeAggregates(pay, settle, fee, network, fxQuote, paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED) if debit.GetCurrency() != "USD" || debit.GetAmount() != "115" { t.Fatalf("expected debit 115 USD, got %s %s", debit.GetCurrency(), debit.GetAmount()) } @@ -71,7 +71,7 @@ func TestComputeAggregatesRecipientPaysFee(t *testing.T) { }, } - debit, settlement := computeAggregates(pay, settle, fee, nil, fxQuote, sharedv1.SettlementMode_SETTLEMENT_FIX_SOURCE) + debit, settlement := computeAggregates(pay, settle, fee, nil, fxQuote, paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE) if debit.GetCurrency() != "USDT" || debit.GetAmount() != "100" { t.Fatalf("expected debit 100 USDT, got %s %s", debit.GetCurrency(), debit.GetAmount()) } diff --git a/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go b/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go index 971c01f6..7eac6bb5 100644 --- a/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go +++ b/api/payments/orchestrator/internal/service/orchestrator/service_helpers_test.go @@ -14,6 +14,7 @@ import ( "github.com/tech/sendico/pkg/model/account_role" paymenttypes "github.com/tech/sendico/pkg/payments/types" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" connectorv1 "github.com/tech/sendico/pkg/proto/connector/v1" ledgerv1 "github.com/tech/sendico/pkg/proto/ledger/v1" orchestratorv1 "github.com/tech/sendico/pkg/proto/payments/orchestration/v1" @@ -60,7 +61,7 @@ func TestNewPayment(t *testing.T) { intent := &sharedv1.PaymentIntent{ Ref: "ref-1", Amount: &moneyv1.Money{Currency: "USD", Amount: "10"}, - SettlementMode: sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED, + SettlementMode: paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED, SettlementCurrency: "USD", } quote := &sharedv1.PaymentQuote{QuoteRef: "q1"} diff --git a/api/payments/quotation/internal/service/quotation/convert.go b/api/payments/quotation/internal/service/quotation/convert.go index 5d31a541..452690fd 100644 --- a/api/payments/quotation/internal/service/quotation/convert.go +++ b/api/payments/quotation/internal/service/quotation/convert.go @@ -11,6 +11,7 @@ import ( 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" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" @@ -293,25 +294,25 @@ func modelKindFromProto(kind sharedv1.PaymentKind) model.PaymentKind { } } -func settlementModeFromProto(mode sharedv1.SettlementMode) model.SettlementMode { +func settlementModeFromProto(mode paymentv1.SettlementMode) model.SettlementMode { switch mode { - case sharedv1.SettlementMode_SETTLEMENT_FIX_SOURCE: + case paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE: return model.SettlementModeFixSource - case sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: + case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: return model.SettlementModeFixReceived default: return model.SettlementModeUnspecified } } -func settlementModeToProto(mode model.SettlementMode) sharedv1.SettlementMode { +func settlementModeToProto(mode model.SettlementMode) paymentv1.SettlementMode { switch mode { case model.SettlementModeFixSource: - return sharedv1.SettlementMode_SETTLEMENT_FIX_SOURCE + return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE case model.SettlementModeFixReceived: - return sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED + return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED default: - return sharedv1.SettlementMode_SETTLEMENT_UNSPECIFIED + return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED } } diff --git a/api/payments/quotation/internal/service/quotation/helpers.go b/api/payments/quotation/internal/service/quotation/helpers.go index 4389fa22..1bc1b1ec 100644 --- a/api/payments/quotation/internal/service/quotation/helpers.go +++ b/api/payments/quotation/internal/service/quotation/helpers.go @@ -8,7 +8,6 @@ import ( oracleclient "github.com/tech/sendico/fx/oracle/client" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" oraclev1 "github.com/tech/sendico/pkg/proto/oracle/v1" - sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" "google.golang.org/protobuf/proto" "google.golang.org/protobuf/types/known/timestamppb" @@ -16,6 +15,7 @@ import ( 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" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" ) type moneyGetter interface { @@ -174,7 +174,7 @@ func resolveTradeAmounts(intentAmount *moneyv1.Money, fxQuote *oraclev1.Quote, s } } -func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote, mode sharedv1.SettlementMode) (*moneyv1.Money, *moneyv1.Money) { +func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.EstimateTransferFeeResponse, fxQuote *oraclev1.Quote, mode paymentv1.SettlementMode) (*moneyv1.Money, *moneyv1.Money) { if pay == nil { return nil, nil } @@ -216,7 +216,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est } switch mode { - case sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: + case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: // Sender pays the fee: keep settlement fixed, increase debit. applyChargeToDebit(fee) default: @@ -226,7 +226,7 @@ func computeAggregates(pay, settlement, fee *moneyv1.Money, network *chainv1.Est if network != nil && network.GetNetworkFee() != nil { switch mode { - case sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: + case paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED: applyChargeToDebit(network.GetNetworkFee()) default: applyChargeToSettlement(network.GetNetworkFee()) diff --git a/api/server/internal/server/paymentapiimp/mapper.go b/api/server/internal/server/paymentapiimp/mapper.go index 41d27388..883a9799 100644 --- a/api/server/internal/server/paymentapiimp/mapper.go +++ b/api/server/internal/server/paymentapiimp/mapper.go @@ -8,6 +8,7 @@ import ( paymenttypes "github.com/tech/sendico/pkg/payments/types" fxv1 "github.com/tech/sendico/pkg/proto/common/fx/v1" moneyv1 "github.com/tech/sendico/pkg/proto/common/money/v1" + paymentv1 "github.com/tech/sendico/pkg/proto/common/payment/v1" chainv1 "github.com/tech/sendico/pkg/proto/gateway/chain/v1" sharedv1 "github.com/tech/sendico/pkg/proto/payments/shared/v1" "github.com/tech/sendico/server/interface/api/srequest" @@ -325,16 +326,16 @@ func mapPaymentKind(kind srequest.PaymentKind) (sharedv1.PaymentKind, error) { } } -func mapSettlementMode(mode srequest.SettlementMode) (sharedv1.SettlementMode, error) { +func mapSettlementMode(mode srequest.SettlementMode) (paymentv1.SettlementMode, error) { switch strings.TrimSpace(string(mode)) { case "", string(srequest.SettlementModeUnspecified): - return sharedv1.SettlementMode_SETTLEMENT_UNSPECIFIED, nil + return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED, nil case string(srequest.SettlementModeFixSource): - return sharedv1.SettlementMode_SETTLEMENT_FIX_SOURCE, nil + return paymentv1.SettlementMode_SETTLEMENT_FIX_SOURCE, nil case string(srequest.SettlementModeFixReceived): - return sharedv1.SettlementMode_SETTLEMENT_FIX_RECEIVED, nil + return paymentv1.SettlementMode_SETTLEMENT_FIX_RECEIVED, nil default: - return sharedv1.SettlementMode_SETTLEMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported settlement mode: " + string(mode)) + return paymentv1.SettlementMode_SETTLEMENT_UNSPECIFIED, merrors.InvalidArgument("unsupported settlement mode: " + string(mode)) } } -- 2.49.1 From 71296800efe09f8189e8fbb6f06991270049317d Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 15:53:52 +0100 Subject: [PATCH 19/21] test fix --- .../server/fileserviceimp/storage/localfs_test.go | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/api/server/internal/server/fileserviceimp/storage/localfs_test.go b/api/server/internal/server/fileserviceimp/storage/localfs_test.go index 68abc372..da1a13bd 100644 --- a/api/server/internal/server/fileserviceimp/storage/localfs_test.go +++ b/api/server/internal/server/fileserviceimp/storage/localfs_test.go @@ -369,9 +369,18 @@ func TestCreateLocalFileStorage(t *testing.T) { } func TestCreateLocalFileStorage_InvalidPath(t *testing.T) { + // Build a deterministic failure case: the target path already exists as a file. + tempDir, err := os.MkdirTemp("", "storage_invalid_path_test") + require.NoError(t, err) + defer os.RemoveAll(tempDir) + + fileAtTargetPath := filepath.Join(tempDir, "test") + err = os.WriteFile(fileAtTargetPath, []byte("not a directory"), 0o644) + require.NoError(t, err) + logger := zap.NewNop() cfg := config.LocalFSSConfig{ - RootPath: "/invalid/path/that/does/not/exist/and/should/fail", + RootPath: tempDir, } storage, err := CreateLocalFileStorage(logger, mservice.Storage, "test", "sub", &mockDomainProvider{}, cfg) -- 2.49.1 From 89ac29968829c377eac85a09b2cec87d0a529ccf Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 16:47:04 +0100 Subject: [PATCH 20/21] fixed error definition --- api/fx/storage/storage.go | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/api/fx/storage/storage.go b/api/fx/storage/storage.go index 691f3783..01a95c7f 100644 --- a/api/fx/storage/storage.go +++ b/api/fx/storage/storage.go @@ -2,22 +2,17 @@ package storage import ( "context" + "errors" "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") + ErrQuoteExpired = errors.New("fx.storage: quote expired") + ErrQuoteConsumed = errors.New("fx.storage: quote consumed") + ErrQuoteNotFirm = errors.New("fx.storage: quote is not firm") + ErrQuoteConsumptionRace = errors.New("fx.storage: quote consumption collision") ) type Repository interface { -- 2.49.1 From 5dd1b5f4d770e9ff777bf853a7f031cdfc50f425 Mon Sep 17 00:00:00 2001 From: Stephan D Date: Fri, 13 Feb 2026 17:01:16 +0100 Subject: [PATCH 21/21] fixed error definition --- api/payments/storage/storage.go | 19 +++++++------------ 1 file changed, 7 insertions(+), 12 deletions(-) diff --git a/api/payments/storage/storage.go b/api/payments/storage/storage.go index 4a5a9085..6c5d946f 100644 --- a/api/payments/storage/storage.go +++ b/api/payments/storage/storage.go @@ -2,6 +2,7 @@ package storage import ( "context" + "errors" "github.com/tech/sendico/payments/storage/model" quotestorage "github.com/tech/sendico/payments/storage/quote" @@ -9,25 +10,19 @@ import ( "go.mongodb.org/mongo-driver/v2/bson" ) -type storageError string - -func (e storageError) Error() string { - return string(e) -} - var ( // ErrPaymentNotFound signals that a payment record does not exist. - ErrPaymentNotFound = storageError("payments.storage: payment not found") + ErrPaymentNotFound = errors.New("payments.storage: payment not found") // ErrDuplicatePayment signals that idempotency constraints were violated. - ErrDuplicatePayment = storageError("payments.storage: duplicate payment") + ErrDuplicatePayment = errors.New("payments.storage: duplicate payment") // ErrRouteNotFound signals that a payment route record does not exist. - ErrRouteNotFound = storageError("payments.storage: route not found") + ErrRouteNotFound = errors.New("payments.storage: route not found") // ErrDuplicateRoute signals that a route already exists for the same transition. - ErrDuplicateRoute = storageError("payments.storage: duplicate route") + ErrDuplicateRoute = errors.New("payments.storage: duplicate route") // ErrPlanTemplateNotFound signals that a plan template record does not exist. - ErrPlanTemplateNotFound = storageError("payments.storage: plan template not found") + ErrPlanTemplateNotFound = errors.New("payments.storage: plan template not found") // ErrDuplicatePlanTemplate signals that a plan template already exists for the same transition. - ErrDuplicatePlanTemplate = storageError("payments.storage: duplicate plan template") + ErrDuplicatePlanTemplate = errors.New("payments.storage: duplicate plan template") ) var ( -- 2.49.1