Skip to content

Commit f079742

Browse files
authored
Merge pull request #3371 from cugxuan/feat/subscription-affiliate-rebate
feat: apply affiliate rebate to subscription payments
2 parents a560d2f + 0fa604b commit f079742

2 files changed

Lines changed: 421 additions & 24 deletions

File tree

backend/internal/service/payment_fulfillment.go

Lines changed: 78 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -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

454462
func (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+
530551
func (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+
567621
func (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

Comments
 (0)