diff --git a/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala b/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala index 154b24e..505381c 100644 --- a/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala +++ b/common/src/main/scala/app/softnetwork/payment/message/PaymentMessages.scala @@ -21,7 +21,9 @@ object PaymentMessages { // Story 13.7 — every payment command carries `var correlationId` + `withCorrelationId` via // AuditableCommand (zero constructor churn). The checkout endpoints stamp it from the inbound // X-Correlation-Id; the handlers thread it onto the persisted payment events (the durable hop). - trait PaymentCommand extends AuditableCommand + trait PaymentCommand extends AuditableCommand { + override type T = PaymentCommand + } trait PaymentCommandWithKey extends PaymentCommand { def key: String diff --git a/common/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala b/common/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala index 44b1467..c684f8f 100644 --- a/common/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala +++ b/common/src/main/scala/app/softnetwork/payment/service/BasicPaymentService.scala @@ -27,6 +27,16 @@ trait BasicPaymentService extends Service[PaymentCommand, PaymentResult] { super.run(command.key, command) } + /** Story 13.7 — keyed convenience over the shared `Service.runCorrelated`: tapir `serverLogic` + * reads the cid as DATA via `HttpCorrelation.correlationInput` (MDC does not survive the + * `serverLogic` `Future` — C14) and dispatches through here, so every keyed command stamps the + * id in one place instead of an inline `cmd.withCorrelationId(cid); run(cmd)` per endpoint. + */ + def runCorrelated(command: PaymentCommandWithKey, correlationId: String)(implicit + tTag: ClassTag[PaymentCommand] + ): Future[PaymentResult] = + runCorrelated(command.key, command, correlationId) + def error(result: PaymentResult): ApiErrors.ErrorInfo = result match { case PaymentAccountNotFound => ApiErrors.NotFound(PaymentAccountNotFound.message) diff --git a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala index 53fda70..0b2301d 100644 --- a/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala +++ b/core/src/main/scala/app/softnetwork/payment/persistence/typed/PaymentBehavior.scala @@ -1028,10 +1028,14 @@ trait PaymentBehavior case Some(paymentAccount) => val recurringPaymentRegistrationId = key.split("#").last Effect.none.thenRun(_ => { - context.self ! ExecuteNextRecurringPayment( + val next = ExecuteNextRecurringPayment( recurringPaymentRegistrationId, paymentAccount.externalUuid ) + // Story 13.7 (DN3) — carry the schedule's correlation id onto the renewal charge so + // the cid set at registration propagates to the scheduled "next recurring" events. + cmd.schedule.correlationId.filter(_.nonEmpty).foreach(next.withCorrelationId) + context.self ! next Schedule4PaymentTriggered(cmd.schedule) ~> replyTo }) case _ => diff --git a/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala index 7e559a2..be7d8e5 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/BankAccountEndpoints.scala @@ -46,8 +46,7 @@ trait BankAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { clientId = client.map(_.clientId).orElse(session.clientId), bankTokenId = bankTokenId ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case r: BankAccountCreatedOrUpdated => Right(r) case other => Left(error(other)) } @@ -71,8 +70,7 @@ trait BankAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { externalUuidWithProfile(session), clientId = client.map(_.clientId).orElse(session.clientId) ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case r: BankAccountLoaded => Right(r.bankAccount.view) case other => Left(error(other)) } @@ -92,8 +90,7 @@ trait BankAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { externalUuidWithProfile(principal._2), Some(false) ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case BankAccountDeleted => Right(BankAccountDeleted) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/BillingPortalEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/BillingPortalEndpoints.scala index cca1b32..49ddb38 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/BillingPortalEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/BillingPortalEndpoints.scala @@ -37,8 +37,7 @@ trait BillingPortalEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { req.returnUrl, clientId = client.map(_.clientId).orElse(session.clientId) ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case r: BillingPortalSessionCreated => Right(r) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala index 110d9d8..7f02f81 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/CheckoutEndpoints.scala @@ -98,8 +98,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { paymentMethodId = paymentMethodId, registerMeansOfPayment = registerMeansOfPayment ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case result: PaymentPreAuthorized => Right(result) case result: PaymentRedirection => Right(result) case result: PaymentRequired => Right(result) @@ -150,8 +149,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { val printReceipt = params.get("printReceipt").getOrElse("false").toBoolean val cmd = PreAuthorizeCallback(orderUuid, preAuthorizationId, registerMeansOfPayment, printReceipt) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case result: PaymentPreAuthorized => Right(result) case result: PaymentRedirection => Right(result) case other => Left(error(other)) @@ -215,8 +213,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { ) // Story 13.7 — stamp the origin correlation id (from X-Correlation-Id, generated if absent) // so it rides the command → the persisted PaidInEvent → the licensing pod. - cmd.withCorrelationId(correlationId) - run(cmd).map { + runCorrelated(cmd, correlationId).map { case result: PaidIn => Right(result) case result: PaymentRedirection => Right(result) case result: PaymentRequired => Right(result) @@ -259,8 +256,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { params.get("registerMeansOfPayment").getOrElse("false").toBoolean val printReceipt = params.get("printReceipt").getOrElse("false").toBoolean val cmd = PayInCallback(orderUuid, transactionId, registerMeansOfPayment, printReceipt) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case result: PaidIn => Right(result) case result: PaymentRedirection => Right(result) case result: PaymentRequired => Right(result) @@ -310,8 +306,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { browserInfo, statementDescriptor ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case result: FirstRecurringPaidIn => Right(result) case result: PaymentRedirection => Right(result) case other => Left(error(other)) @@ -347,8 +342,7 @@ trait CheckoutEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { .serverLogic { case (recurringPayInRegistrationId, transactionId, cid) => val cmd = RecurringPaymentCallback(recurringPayInRegistrationId, transactionId) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case result: PaidIn => Right(result) case result: PaymentRedirection => Right(result) case other => Left(error(other)) diff --git a/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala index 451267b..6c51137 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/KycDocumentEndpoints.scala @@ -48,8 +48,7 @@ trait KycDocumentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { case Some(kycDocumentType) => val cmd = LoadKycDocumentStatus(externalUuidWithProfile(principal._2), kycDocumentType) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case r: KycDocumentStatusLoaded => Right(r.report) case other => Left(error(other)) } @@ -85,8 +84,7 @@ trait KycDocumentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { case Some(kycDocumentType) => val cmd = AddKycDocument(externalUuidWithProfile(principal._2), pages.bytes, kycDocumentType) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd) + runCorrelated(cmd, cid) .map { case r: KycDocumentAdded => Right(r) case other => Left(error(other)) diff --git a/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala index d5bd544..d0fd8a1 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/MandateEndpoints.scala @@ -46,8 +46,7 @@ trait MandateEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { iban = maybeIban.map(_.iban), clientId = principal._1.map(_.clientId).orElse(principal._2.clientId) ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case MandateCreated => Right(MandateCreated) case r: MandateConfirmationRequired => Right(r) case other => Left(error(other)) @@ -70,8 +69,7 @@ trait MandateEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { externalUuidWithProfile(session), clientId = client.map(_.clientId).orElse(session.clientId) ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case MandateCanceled => Right(MandateCanceled) case other => Left(error(other)) } @@ -90,8 +88,7 @@ trait MandateEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { .description("Update mandate status web hook") .serverLogic { case (mandateId, cid) => val cmd = UpdateMandateStatus(mandateId) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case r: MandateStatusUpdated => Right(r.result) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/PaymentAccountEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/PaymentAccountEndpoints.scala index 63fe05f..25f2c2f 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/PaymentAccountEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/PaymentAccountEndpoints.scala @@ -84,8 +84,7 @@ trait PaymentAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { userAgent = userAgent, tokenId = tokenId ) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case r: UserPaymentAccountCreatedOrUpdated => Right(r) case other => Left(error(other)) } @@ -105,8 +104,7 @@ trait PaymentAccountEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { externalUuidWithProfile(session), clientId = client.map(_.clientId).orElse(session.clientId) ) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case r: PaymentAccountLoaded => Right(r.paymentAccount.view) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/PaymentMethodEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/PaymentMethodEndpoints.scala index 2d224dc..366b17d 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/PaymentMethodEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/PaymentMethodEndpoints.scala @@ -32,8 +32,7 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { cid => { val cmd = LoadPaymentMethods(externalUuidWithProfile(principal._2)) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case r: PaymentMethodsLoaded => Right(PaymentMethodsView(r.paymentMethods).cards) case other => Left(error(other)) } @@ -54,8 +53,7 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { cid => { val cmd = LoadPaymentMethods(externalUuidWithProfile(principal._2)) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case r: PaymentMethodsLoaded => Right(PaymentMethodsView(r.paymentMethods)) case other => Left(error(other)) } @@ -77,7 +75,6 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { .serverLogic(principal => args => { val cmd = args._1 - cmd.withCorrelationId(args._2) // Story 13.7 — origin stamp var updatedUser = if (cmd.user.externalUuid.trim.isEmpty) { cmd.user.withExternalUuid(principal._2.id) @@ -89,12 +86,13 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { updatedUser = updatedUser.withProfile(profile) case _ => } - run( + runCorrelated( cmd.copy( user = updatedUser, paymentType = Transaction.PaymentType.CARD, clientId = principal._1.map(_.clientId).orElse(principal._2.clientId) - ) + ), + args._2 ).map { case r: PaymentMethodPreRegistered => Right(r.preRegistration) case other => Left(error(other)) @@ -117,7 +115,6 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { .serverLogic(principal => args => { val cmd = args._1 - cmd.withCorrelationId(args._2) // Story 13.7 — origin stamp var updatedUser = if (cmd.user.externalUuid.trim.isEmpty) { cmd.user.withExternalUuid(principal._2.id) @@ -129,11 +126,12 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { updatedUser = updatedUser.withProfile(profile) case _ => } - run( + runCorrelated( cmd.copy( user = updatedUser, clientId = principal._1.map(_.clientId).orElse(principal._2.clientId) - ) + ), + args._2 ).map { case r: PaymentMethodPreRegistered => Right(r.preRegistration) case other => Left(error(other)) @@ -156,8 +154,7 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { externalUuidWithProfile(principal._2), args._1 ) - cmd.withCorrelationId(args._2) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, args._2).map { case PaymentMethodDisabled => Right(PaymentMethodDisabled) case other => Left(error(other)) } @@ -179,8 +176,7 @@ trait PaymentMethodEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { externalUuidWithProfile(principal._2), args._1 ) - cmd.withCorrelationId(args._2) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, args._2).map { case PaymentMethodDisabled => Right(PaymentMethodDisabled) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala index f54013c..db3ab1e 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/RecurringPaymentEndpoints.scala @@ -49,8 +49,7 @@ trait RecurringPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] debitedAccount = externalUuidWithProfile(session), clientId = client.map(_.clientId).orElse(session.clientId) ) - updated.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(updated).map { + runCorrelated(updated, correlationId).map { case r: RecurringPaymentRegistered => Right(r) case r: MandateConfirmationRequired => Right(r) case other => Left(error(other)) @@ -101,8 +100,7 @@ trait RecurringPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] ) .serverLogic(principal => { case (cmd, correlationId) => val updated = cmd.copy(debitedAccount = externalUuidWithProfile(principal._2)) - updated.withCorrelationId(correlationId) // Story 13.7 — origin stamp (after copy) - run(updated).map { + runCorrelated(updated, correlationId).map { case r: RecurringCardPaymentRegistrationUpdated => Right(r.result) case other => Left(error(other)) } @@ -128,8 +126,7 @@ trait RecurringPaymentEndpoints[SD <: SessionData with SessionDataDecorator[SD]] None, Some(RecurringPayment.RecurringCardPaymentStatus.ENDED) ) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case r: RecurringCardPaymentRegistrationUpdated => Right(r.result) case other => Left(error(other)) } diff --git a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala index a0a83bd..227f6da 100644 --- a/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala +++ b/core/src/main/scala/app/softnetwork/payment/service/UboDeclarationEndpoints.scala @@ -39,8 +39,7 @@ trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { val correlationId = args._2 val cmd = CreateOrUpdateUbo(externalUuidWithProfile(principal._2), ubo) - cmd.withCorrelationId(correlationId) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, correlationId).map { case r: UboCreatedOrUpdated => Right(r.ubo) case other => Left(error(other)) } @@ -61,7 +60,7 @@ trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { .serverLogic(principal => cid => { val cmd = GetUboDeclaration(externalUuidWithProfile(principal._2)) - run(cmd).map { + runCorrelated(cmd, cid).map { case r: UboDeclarationLoaded => Right(r.declaration.view) case other => Left(error(other)) } @@ -95,8 +94,7 @@ trait UboDeclarationEndpoints[SD <: SessionData with SessionDataDecorator[SD]] { userAgent, tokenId ) - cmd.withCorrelationId(cid) // Story 13.7 — origin stamp - run(cmd).map { + runCorrelated(cmd, cid).map { case UboDeclarationAskedForValidation => Right(UboDeclarationAskedForValidation) case other => Left(error(other)) }