@@ -11,6 +11,8 @@ import (
1111 "strings"
1212 "time"
1313
14+ "entgo.io/ent/dialect"
15+
1416 dbent "github.com/Wei-Shaw/sub2api/ent"
1517 "github.com/Wei-Shaw/sub2api/ent/paymentauditlog"
1618 "github.com/Wei-Shaw/sub2api/ent/paymentorder"
@@ -429,16 +431,22 @@ func (s *PaymentService) doSub(ctx context.Context, o *dbent.PaymentOrder) error
429431 if err != nil || g .Status != payment .EntityStatusActive {
430432 return fmt .Errorf ("group %d no longer exists or inactive" , gid )
431433 }
432- // Idempotency: check audit log to see if subscription was already assigned.
433- // Prevents double-extension on retry after markCompleted fails.
434- if s .hasAuditLog (ctx , o .ID , "SUBSCRIPTION_SUCCESS" ) {
434+ assigned := s .hasAuditLog (ctx , o .ID , "SUBSCRIPTION_ASSIGNED" ) || s .hasAuditLog (ctx , o .ID , "SUBSCRIPTION_SUCCESS" )
435+ if ! assigned {
436+ orderNote := fmt .Sprintf ("payment order %d" , o .ID )
437+ _ , _ , err = s .subscriptionSvc .AssignOrExtendSubscription (ctx , & AssignSubscriptionInput {UserID : o .UserID , GroupID : gid , ValidityDays : days , AssignedBy : 0 , Notes : orderNote })
438+ if err != nil {
439+ return fmt .Errorf ("assign subscription: %w" , err )
440+ }
441+ s .writeAuditLog (ctx , o .ID , "SUBSCRIPTION_ASSIGNED" , "system" , map [string ]any {
442+ "groupID" : gid ,
443+ "validityDays" : days ,
444+ })
445+ } else {
435446 slog .Info ("subscription already assigned for order, skipping" , "orderID" , o .ID , "groupID" , gid )
436- return s .markCompleted (ctx , o , "SUBSCRIPTION_SUCCESS" )
437447 }
438- orderNote := fmt .Sprintf ("payment order %d" , o .ID )
439- _ , _ , err = s .subscriptionSvc .AssignOrExtendSubscription (ctx , & AssignSubscriptionInput {UserID : o .UserID , GroupID : gid , ValidityDays : days , AssignedBy : 0 , Notes : orderNote })
440- if err != nil {
441- return fmt .Errorf ("assign subscription: %w" , err )
448+ if err := s .applyAffiliateRebateForOrder (ctx , o ); err != nil {
449+ return err
442450 }
443451 return s .markCompleted (ctx , o , "SUBSCRIPTION_SUCCESS" )
444452}
@@ -452,7 +460,8 @@ func (s *PaymentService) hasAuditLog(ctx context.Context, orderID int64, action
452460}
453461
454462func (s * PaymentService ) applyAffiliateRebateForOrder (ctx context.Context , o * dbent.PaymentOrder ) error {
455- if o == nil || o .OrderType != payment .OrderTypeBalance || o .Amount <= 0 {
463+ baseAmount := affiliateRebateBaseAmount (o )
464+ if o == nil || baseAmount <= 0 {
456465 return nil
457466 }
458467 if s .affiliateService == nil {
@@ -469,7 +478,7 @@ func (s *PaymentService) applyAffiliateRebateForOrder(ctx context.Context, o *db
469478 defer func () { _ = tx .Rollback () }()
470479
471480 txCtx := dbent .NewTxContext (ctx , tx )
472- claimed , err := s .tryClaimAffiliateRebateAudit (txCtx , tx .Client (), o .ID , o . Amount )
481+ claimed , err := s .tryClaimAffiliateRebateAudit (txCtx , tx .Client (), o .ID , baseAmount )
473482 if err != nil {
474483 s .writeAuditLog (ctx , o .ID , "AFFILIATE_REBATE_FAILED" , "system" , map [string ]any {
475484 "error" : err .Error (),
@@ -481,7 +490,7 @@ func (s *PaymentService) applyAffiliateRebateForOrder(ctx context.Context, o *db
481490 }
482491
483492 sourceOrderID := o .ID
484- rebateAmount , err := s .affiliateService .AccrueInviteRebateForOrder (txCtx , o .UserID , o . Amount , & sourceOrderID )
493+ rebateAmount , err := s .affiliateService .AccrueInviteRebateForOrder (txCtx , o .UserID , baseAmount , & sourceOrderID )
485494 if err != nil {
486495 s .writeAuditLog (ctx , o .ID , "AFFILIATE_REBATE_FAILED" , "system" , map [string ]any {
487496 "error" : err .Error (),
@@ -491,7 +500,7 @@ func (s *PaymentService) applyAffiliateRebateForOrder(ctx context.Context, o *db
491500
492501 if rebateAmount <= 0 {
493502 if err := s .updateClaimedAffiliateRebateAudit (txCtx , tx .Client (), o .ID , "AFFILIATE_REBATE_SKIPPED" , map [string ]any {
494- "baseAmount" : o . Amount ,
503+ "baseAmount" : baseAmount ,
495504 "reason" : "no inviter bound or rebate amount <= 0" ,
496505 }); err != nil {
497506 s .writeAuditLog (ctx , o .ID , "AFFILIATE_REBATE_FAILED" , "system" , map [string ]any {
@@ -509,7 +518,7 @@ func (s *PaymentService) applyAffiliateRebateForOrder(ctx context.Context, o *db
509518 }
510519
511520 if err := s .updateClaimedAffiliateRebateAudit (txCtx , tx .Client (), o .ID , "AFFILIATE_REBATE_APPLIED" , map [string ]any {
512- "baseAmount" : o . Amount ,
521+ "baseAmount" : baseAmount ,
513522 "rebateAmount" : rebateAmount ,
514523 }); err != nil {
515524 s .writeAuditLog (ctx , o .ID , "AFFILIATE_REBATE_FAILED" , "system" , map [string ]any {
@@ -527,6 +536,18 @@ func (s *PaymentService) applyAffiliateRebateForOrder(ctx context.Context, o *db
527536 return nil
528537}
529538
539+ func affiliateRebateBaseAmount (o * dbent.PaymentOrder ) float64 {
540+ if o == nil {
541+ return 0
542+ }
543+ switch o .OrderType {
544+ case payment .OrderTypeBalance , payment .OrderTypeSubscription :
545+ return o .Amount
546+ default :
547+ return 0
548+ }
549+ }
550+
530551func (s * PaymentService ) tryClaimAffiliateRebateAudit (ctx context.Context , client * dbent.Client , orderID int64 , baseAmount float64 ) (bool , error ) {
531552 if client == nil {
532553 return false , errors .New ("nil payment client" )
@@ -536,17 +557,8 @@ func (s *PaymentService) tryClaimAffiliateRebateAudit(ctx context.Context, clien
536557 "baseAmount" : baseAmount ,
537558 "status" : "reserved" ,
538559 })
539- rows , err := client .QueryContext (ctx , `
540- INSERT INTO payment_audit_logs (order_id, action, detail, operator, created_at)
541- SELECT $1::text, 'AFFILIATE_REBATE_APPLIED', $2::text, 'system', NOW()
542- WHERE NOT EXISTS (
543- SELECT 1
544- FROM payment_audit_logs
545- WHERE order_id = $1::text
546- AND action IN ('AFFILIATE_REBATE_APPLIED', 'AFFILIATE_REBATE_SKIPPED')
547- )
548- ON CONFLICT (order_id, action) DO NOTHING
549- RETURNING id` , oid , string (detail ))
560+ query , args := buildAffiliateRebateAuditClaimQuery (client , oid , string (detail ))
561+ rows , err := client .QueryContext (ctx , query , args ... )
550562 if err != nil {
551563 return false , err
552564 }
@@ -564,6 +576,48 @@ RETURNING id`, oid, string(detail))
564576 return true , nil
565577}
566578
579+ func buildAffiliateRebateAuditClaimQuery (client * dbent.Client , orderID , detail string ) (string , []any ) {
580+ nowExpr := paymentAuditCurrentTimestampExpr (client )
581+ if paymentAuditDialect (client ) == dialect .Postgres {
582+ return fmt .Sprintf (`
583+ INSERT INTO payment_audit_logs (order_id, action, detail, operator, created_at)
584+ SELECT $1::text, 'AFFILIATE_REBATE_APPLIED', $2::text, 'system', %s
585+ WHERE NOT EXISTS (
586+ SELECT 1
587+ FROM payment_audit_logs
588+ WHERE order_id = $1::text
589+ AND action IN ('AFFILIATE_REBATE_APPLIED', 'AFFILIATE_REBATE_SKIPPED')
590+ )
591+ ON CONFLICT (order_id, action) DO NOTHING
592+ RETURNING id` , nowExpr ), []any {orderID , detail }
593+ }
594+ return fmt .Sprintf (`
595+ INSERT INTO payment_audit_logs (order_id, action, detail, operator, created_at)
596+ SELECT ?, 'AFFILIATE_REBATE_APPLIED', ?, 'system', %s
597+ WHERE NOT EXISTS (
598+ SELECT 1
599+ FROM payment_audit_logs
600+ WHERE order_id = ?
601+ AND action IN ('AFFILIATE_REBATE_APPLIED', 'AFFILIATE_REBATE_SKIPPED')
602+ )
603+ ON CONFLICT (order_id, action) DO NOTHING
604+ RETURNING id` , nowExpr ), []any {orderID , detail , orderID }
605+ }
606+
607+ func paymentAuditCurrentTimestampExpr (client * dbent.Client ) string {
608+ if paymentAuditDialect (client ) == dialect .Postgres {
609+ return "NOW()"
610+ }
611+ return "CURRENT_TIMESTAMP"
612+ }
613+
614+ func paymentAuditDialect (client * dbent.Client ) string {
615+ if client == nil || client .Driver () == nil {
616+ return ""
617+ }
618+ return client .Driver ().Dialect ()
619+ }
620+
567621func (s * PaymentService ) updateClaimedAffiliateRebateAudit (ctx context.Context , client * dbent.Client , orderID int64 , action string , detail map [string ]any ) error {
568622 if client == nil {
569623 return errors .New ("nil payment client" )
0 commit comments