From c0b24aefba926c61d749af11cd01cd6f96bb65fe Mon Sep 17 00:00:00 2001 From: IanShaw027 Date: Mon, 20 Apr 2026 20:47:14 +0800 Subject: [PATCH] feat: snapshot payment provider keys on orders --- backend/ent/migrate/schema.go | 15 ++-- backend/ent/mutation.go | 75 ++++++++++++++++- backend/ent/paymentorder.go | 16 +++- backend/ent/paymentorder/paymentorder.go | 10 +++ backend/ent/paymentorder/where.go | 80 ++++++++++++++++++ backend/ent/paymentorder_create.go | 83 +++++++++++++++++++ backend/ent/paymentorder_update.go | 62 ++++++++++++++ backend/ent/runtime/runtime.go | 20 +++-- backend/ent/schema/payment_order.go | 4 + .../internal/service/payment_fulfillment.go | 7 +- .../service/payment_fulfillment_test.go | 21 ++++- backend/internal/service/payment_order.go | 8 +- .../service/payment_order_lifecycle.go | 13 ++- ...dd_payment_order_provider_key_snapshot.sql | 10 +++ 14 files changed, 400 insertions(+), 24 deletions(-) create mode 100644 backend/migrations/112_add_payment_order_provider_key_snapshot.sql diff --git a/backend/ent/migrate/schema.go b/backend/ent/migrate/schema.go index bf41e73b..230ea060 100644 --- a/backend/ent/migrate/schema.go +++ b/backend/ent/migrate/schema.go @@ -654,6 +654,7 @@ var ( {Name: "subscription_group_id", Type: field.TypeInt64, Nullable: true}, {Name: "subscription_days", Type: field.TypeInt, Nullable: true}, {Name: "provider_instance_id", Type: field.TypeString, Nullable: true, Size: 64}, + {Name: "provider_key", Type: field.TypeString, Nullable: true, Size: 30}, {Name: "status", Type: field.TypeString, Size: 30, Default: "PENDING"}, {Name: "refund_amount", Type: field.TypeFloat64, Default: 0, SchemaType: map[string]string{"postgres": "decimal(20,2)"}}, {Name: "refund_reason", Type: field.TypeString, Nullable: true, SchemaType: map[string]string{"postgres": "text"}}, @@ -682,7 +683,7 @@ var ( ForeignKeys: []*schema.ForeignKey{ { Symbol: "payment_orders_users_payment_orders", - Columns: []*schema.Column{PaymentOrdersColumns[37]}, + Columns: []*schema.Column{PaymentOrdersColumns[38]}, RefColumns: []*schema.Column{UsersColumns[0]}, OnDelete: schema.NoAction, }, @@ -696,32 +697,32 @@ var ( { Name: "paymentorder_user_id", Unique: false, - Columns: []*schema.Column{PaymentOrdersColumns[37]}, + Columns: []*schema.Column{PaymentOrdersColumns[38]}, }, { Name: "paymentorder_status", Unique: false, - Columns: []*schema.Column{PaymentOrdersColumns[19]}, + Columns: []*schema.Column{PaymentOrdersColumns[20]}, }, { Name: "paymentorder_expires_at", Unique: false, - Columns: []*schema.Column{PaymentOrdersColumns[27]}, + Columns: []*schema.Column{PaymentOrdersColumns[28]}, }, { Name: "paymentorder_created_at", Unique: false, - Columns: []*schema.Column{PaymentOrdersColumns[35]}, + Columns: []*schema.Column{PaymentOrdersColumns[36]}, }, { Name: "paymentorder_paid_at", Unique: false, - Columns: []*schema.Column{PaymentOrdersColumns[28]}, + Columns: []*schema.Column{PaymentOrdersColumns[29]}, }, { Name: "paymentorder_payment_type_paid_at", Unique: false, - Columns: []*schema.Column{PaymentOrdersColumns[9], PaymentOrdersColumns[28]}, + Columns: []*schema.Column{PaymentOrdersColumns[9], PaymentOrdersColumns[29]}, }, { Name: "paymentorder_order_type", diff --git a/backend/ent/mutation.go b/backend/ent/mutation.go index 12905c9a..5227015c 100644 --- a/backend/ent/mutation.go +++ b/backend/ent/mutation.go @@ -15385,6 +15385,7 @@ type PaymentOrderMutation struct { subscription_days *int addsubscription_days *int provider_instance_id *string + provider_key *string status *string refund_amount *float64 addrefund_amount *float64 @@ -16421,6 +16422,55 @@ func (m *PaymentOrderMutation) ResetProviderInstanceID() { delete(m.clearedFields, paymentorder.FieldProviderInstanceID) } +// SetProviderKey sets the "provider_key" field. +func (m *PaymentOrderMutation) SetProviderKey(s string) { + m.provider_key = &s +} + +// ProviderKey returns the value of the "provider_key" field in the mutation. +func (m *PaymentOrderMutation) ProviderKey() (r string, exists bool) { + v := m.provider_key + if v == nil { + return + } + return *v, true +} + +// OldProviderKey returns the old "provider_key" field's value of the PaymentOrder entity. +// If the PaymentOrder object wasn't provided to the builder, the object is fetched from the database. +// An error is returned if the mutation operation is not UpdateOne, or the database query fails. +func (m *PaymentOrderMutation) OldProviderKey(ctx context.Context) (v *string, err error) { + if !m.op.Is(OpUpdateOne) { + return v, errors.New("OldProviderKey is only allowed on UpdateOne operations") + } + if m.id == nil || m.oldValue == nil { + return v, errors.New("OldProviderKey requires an ID field in the mutation") + } + oldValue, err := m.oldValue(ctx) + if err != nil { + return v, fmt.Errorf("querying old value for OldProviderKey: %w", err) + } + return oldValue.ProviderKey, nil +} + +// ClearProviderKey clears the value of the "provider_key" field. +func (m *PaymentOrderMutation) ClearProviderKey() { + m.provider_key = nil + m.clearedFields[paymentorder.FieldProviderKey] = struct{}{} +} + +// ProviderKeyCleared returns if the "provider_key" field was cleared in this mutation. +func (m *PaymentOrderMutation) ProviderKeyCleared() bool { + _, ok := m.clearedFields[paymentorder.FieldProviderKey] + return ok +} + +// ResetProviderKey resets all changes to the "provider_key" field. +func (m *PaymentOrderMutation) ResetProviderKey() { + m.provider_key = nil + delete(m.clearedFields, paymentorder.FieldProviderKey) +} + // SetStatus sets the "status" field. func (m *PaymentOrderMutation) SetStatus(s string) { m.status = &s @@ -17280,7 +17330,7 @@ func (m *PaymentOrderMutation) Type() string { // order to get all numeric fields that were incremented/decremented, call // AddedFields(). func (m *PaymentOrderMutation) Fields() []string { - fields := make([]string, 0, 37) + fields := make([]string, 0, 38) if m.user != nil { fields = append(fields, paymentorder.FieldUserID) } @@ -17338,6 +17388,9 @@ func (m *PaymentOrderMutation) Fields() []string { if m.provider_instance_id != nil { fields = append(fields, paymentorder.FieldProviderInstanceID) } + if m.provider_key != nil { + fields = append(fields, paymentorder.FieldProviderKey) + } if m.status != nil { fields = append(fields, paymentorder.FieldStatus) } @@ -17438,6 +17491,8 @@ func (m *PaymentOrderMutation) Field(name string) (ent.Value, bool) { return m.SubscriptionDays() case paymentorder.FieldProviderInstanceID: return m.ProviderInstanceID() + case paymentorder.FieldProviderKey: + return m.ProviderKey() case paymentorder.FieldStatus: return m.Status() case paymentorder.FieldRefundAmount: @@ -17521,6 +17576,8 @@ func (m *PaymentOrderMutation) OldField(ctx context.Context, name string) (ent.V return m.OldSubscriptionDays(ctx) case paymentorder.FieldProviderInstanceID: return m.OldProviderInstanceID(ctx) + case paymentorder.FieldProviderKey: + return m.OldProviderKey(ctx) case paymentorder.FieldStatus: return m.OldStatus(ctx) case paymentorder.FieldRefundAmount: @@ -17699,6 +17756,13 @@ func (m *PaymentOrderMutation) SetField(name string, value ent.Value) error { } m.SetProviderInstanceID(v) return nil + case paymentorder.FieldProviderKey: + v, ok := value.(string) + if !ok { + return fmt.Errorf("unexpected type %T for field %s", value, name) + } + m.SetProviderKey(v) + return nil case paymentorder.FieldStatus: v, ok := value.(string) if !ok { @@ -17966,6 +18030,9 @@ func (m *PaymentOrderMutation) ClearedFields() []string { if m.FieldCleared(paymentorder.FieldProviderInstanceID) { fields = append(fields, paymentorder.FieldProviderInstanceID) } + if m.FieldCleared(paymentorder.FieldProviderKey) { + fields = append(fields, paymentorder.FieldProviderKey) + } if m.FieldCleared(paymentorder.FieldRefundReason) { fields = append(fields, paymentorder.FieldRefundReason) } @@ -18034,6 +18101,9 @@ func (m *PaymentOrderMutation) ClearField(name string) error { case paymentorder.FieldProviderInstanceID: m.ClearProviderInstanceID() return nil + case paymentorder.FieldProviderKey: + m.ClearProviderKey() + return nil case paymentorder.FieldRefundReason: m.ClearRefundReason() return nil @@ -18129,6 +18199,9 @@ func (m *PaymentOrderMutation) ResetField(name string) error { case paymentorder.FieldProviderInstanceID: m.ResetProviderInstanceID() return nil + case paymentorder.FieldProviderKey: + m.ResetProviderKey() + return nil case paymentorder.FieldStatus: m.ResetStatus() return nil diff --git a/backend/ent/paymentorder.go b/backend/ent/paymentorder.go index 6ea3e709..a58823ee 100644 --- a/backend/ent/paymentorder.go +++ b/backend/ent/paymentorder.go @@ -56,6 +56,8 @@ type PaymentOrder struct { SubscriptionDays *int `json:"subscription_days,omitempty"` // ProviderInstanceID holds the value of the "provider_instance_id" field. ProviderInstanceID *string `json:"provider_instance_id,omitempty"` + // ProviderKey holds the value of the "provider_key" field. + ProviderKey *string `json:"provider_key,omitempty"` // Status holds the value of the "status" field. Status string `json:"status,omitempty"` // RefundAmount holds the value of the "refund_amount" field. @@ -129,7 +131,7 @@ func (*PaymentOrder) scanValues(columns []string) ([]any, error) { values[i] = new(sql.NullFloat64) case paymentorder.FieldID, paymentorder.FieldUserID, paymentorder.FieldPlanID, paymentorder.FieldSubscriptionGroupID, paymentorder.FieldSubscriptionDays: values[i] = new(sql.NullInt64) - case paymentorder.FieldUserEmail, paymentorder.FieldUserName, paymentorder.FieldUserNotes, paymentorder.FieldRechargeCode, paymentorder.FieldOutTradeNo, paymentorder.FieldPaymentType, paymentorder.FieldPaymentTradeNo, paymentorder.FieldPayURL, paymentorder.FieldQrCode, paymentorder.FieldQrCodeImg, paymentorder.FieldOrderType, paymentorder.FieldProviderInstanceID, paymentorder.FieldStatus, paymentorder.FieldRefundReason, paymentorder.FieldRefundRequestReason, paymentorder.FieldRefundRequestedBy, paymentorder.FieldFailedReason, paymentorder.FieldClientIP, paymentorder.FieldSrcHost, paymentorder.FieldSrcURL: + case paymentorder.FieldUserEmail, paymentorder.FieldUserName, paymentorder.FieldUserNotes, paymentorder.FieldRechargeCode, paymentorder.FieldOutTradeNo, paymentorder.FieldPaymentType, paymentorder.FieldPaymentTradeNo, paymentorder.FieldPayURL, paymentorder.FieldQrCode, paymentorder.FieldQrCodeImg, paymentorder.FieldOrderType, paymentorder.FieldProviderInstanceID, paymentorder.FieldProviderKey, paymentorder.FieldStatus, paymentorder.FieldRefundReason, paymentorder.FieldRefundRequestReason, paymentorder.FieldRefundRequestedBy, paymentorder.FieldFailedReason, paymentorder.FieldClientIP, paymentorder.FieldSrcHost, paymentorder.FieldSrcURL: values[i] = new(sql.NullString) case paymentorder.FieldRefundAt, paymentorder.FieldRefundRequestedAt, paymentorder.FieldExpiresAt, paymentorder.FieldPaidAt, paymentorder.FieldCompletedAt, paymentorder.FieldFailedAt, paymentorder.FieldCreatedAt, paymentorder.FieldUpdatedAt: values[i] = new(sql.NullTime) @@ -276,6 +278,13 @@ func (_m *PaymentOrder) assignValues(columns []string, values []any) error { _m.ProviderInstanceID = new(string) *_m.ProviderInstanceID = value.String } + case paymentorder.FieldProviderKey: + if value, ok := values[i].(*sql.NullString); !ok { + return fmt.Errorf("unexpected type %T for field provider_key", values[i]) + } else if value.Valid { + _m.ProviderKey = new(string) + *_m.ProviderKey = value.String + } case paymentorder.FieldStatus: if value, ok := values[i].(*sql.NullString); !ok { return fmt.Errorf("unexpected type %T for field status", values[i]) @@ -508,6 +517,11 @@ func (_m *PaymentOrder) String() string { builder.WriteString(*v) } builder.WriteString(", ") + if v := _m.ProviderKey; v != nil { + builder.WriteString("provider_key=") + builder.WriteString(*v) + } + builder.WriteString(", ") builder.WriteString("status=") builder.WriteString(_m.Status) builder.WriteString(", ") diff --git a/backend/ent/paymentorder/paymentorder.go b/backend/ent/paymentorder/paymentorder.go index 4467b2b6..af9b1422 100644 --- a/backend/ent/paymentorder/paymentorder.go +++ b/backend/ent/paymentorder/paymentorder.go @@ -52,6 +52,8 @@ const ( FieldSubscriptionDays = "subscription_days" // FieldProviderInstanceID holds the string denoting the provider_instance_id field in the database. FieldProviderInstanceID = "provider_instance_id" + // FieldProviderKey holds the string denoting the provider_key field in the database. + FieldProviderKey = "provider_key" // FieldStatus holds the string denoting the status field in the database. FieldStatus = "status" // FieldRefundAmount holds the string denoting the refund_amount field in the database. @@ -123,6 +125,7 @@ var Columns = []string{ FieldSubscriptionGroupID, FieldSubscriptionDays, FieldProviderInstanceID, + FieldProviderKey, FieldStatus, FieldRefundAmount, FieldRefundReason, @@ -176,6 +179,8 @@ var ( OrderTypeValidator func(string) error // ProviderInstanceIDValidator is a validator for the "provider_instance_id" field. It is called by the builders before save. ProviderInstanceIDValidator func(string) error + // ProviderKeyValidator is a validator for the "provider_key" field. It is called by the builders before save. + ProviderKeyValidator func(string) error // DefaultStatus holds the default value on creation for the "status" field. DefaultStatus string // StatusValidator is a validator for the "status" field. It is called by the builders before save. @@ -301,6 +306,11 @@ func ByProviderInstanceID(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldProviderInstanceID, opts...).ToFunc() } +// ByProviderKey orders the results by the provider_key field. +func ByProviderKey(opts ...sql.OrderTermOption) OrderOption { + return sql.OrderByField(FieldProviderKey, opts...).ToFunc() +} + // ByStatus orders the results by the status field. func ByStatus(opts ...sql.OrderTermOption) OrderOption { return sql.OrderByField(FieldStatus, opts...).ToFunc() diff --git a/backend/ent/paymentorder/where.go b/backend/ent/paymentorder/where.go index 78520fac..0f6b74a0 100644 --- a/backend/ent/paymentorder/where.go +++ b/backend/ent/paymentorder/where.go @@ -150,6 +150,11 @@ func ProviderInstanceID(v string) predicate.PaymentOrder { return predicate.PaymentOrder(sql.FieldEQ(FieldProviderInstanceID, v)) } +// ProviderKey applies equality check predicate on the "provider_key" field. It's identical to ProviderKeyEQ. +func ProviderKey(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldEQ(FieldProviderKey, v)) +} + // Status applies equality check predicate on the "status" field. It's identical to StatusEQ. func Status(v string) predicate.PaymentOrder { return predicate.PaymentOrder(sql.FieldEQ(FieldStatus, v)) @@ -1360,6 +1365,81 @@ func ProviderInstanceIDContainsFold(v string) predicate.PaymentOrder { return predicate.PaymentOrder(sql.FieldContainsFold(FieldProviderInstanceID, v)) } +// ProviderKeyEQ applies the EQ predicate on the "provider_key" field. +func ProviderKeyEQ(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldEQ(FieldProviderKey, v)) +} + +// ProviderKeyNEQ applies the NEQ predicate on the "provider_key" field. +func ProviderKeyNEQ(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldNEQ(FieldProviderKey, v)) +} + +// ProviderKeyIn applies the In predicate on the "provider_key" field. +func ProviderKeyIn(vs ...string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldIn(FieldProviderKey, vs...)) +} + +// ProviderKeyNotIn applies the NotIn predicate on the "provider_key" field. +func ProviderKeyNotIn(vs ...string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldNotIn(FieldProviderKey, vs...)) +} + +// ProviderKeyGT applies the GT predicate on the "provider_key" field. +func ProviderKeyGT(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldGT(FieldProviderKey, v)) +} + +// ProviderKeyGTE applies the GTE predicate on the "provider_key" field. +func ProviderKeyGTE(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldGTE(FieldProviderKey, v)) +} + +// ProviderKeyLT applies the LT predicate on the "provider_key" field. +func ProviderKeyLT(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldLT(FieldProviderKey, v)) +} + +// ProviderKeyLTE applies the LTE predicate on the "provider_key" field. +func ProviderKeyLTE(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldLTE(FieldProviderKey, v)) +} + +// ProviderKeyContains applies the Contains predicate on the "provider_key" field. +func ProviderKeyContains(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldContains(FieldProviderKey, v)) +} + +// ProviderKeyHasPrefix applies the HasPrefix predicate on the "provider_key" field. +func ProviderKeyHasPrefix(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldHasPrefix(FieldProviderKey, v)) +} + +// ProviderKeyHasSuffix applies the HasSuffix predicate on the "provider_key" field. +func ProviderKeyHasSuffix(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldHasSuffix(FieldProviderKey, v)) +} + +// ProviderKeyIsNil applies the IsNil predicate on the "provider_key" field. +func ProviderKeyIsNil() predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldIsNull(FieldProviderKey)) +} + +// ProviderKeyNotNil applies the NotNil predicate on the "provider_key" field. +func ProviderKeyNotNil() predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldNotNull(FieldProviderKey)) +} + +// ProviderKeyEqualFold applies the EqualFold predicate on the "provider_key" field. +func ProviderKeyEqualFold(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldEqualFold(FieldProviderKey, v)) +} + +// ProviderKeyContainsFold applies the ContainsFold predicate on the "provider_key" field. +func ProviderKeyContainsFold(v string) predicate.PaymentOrder { + return predicate.PaymentOrder(sql.FieldContainsFold(FieldProviderKey, v)) +} + // StatusEQ applies the EQ predicate on the "status" field. func StatusEQ(v string) predicate.PaymentOrder { return predicate.PaymentOrder(sql.FieldEQ(FieldStatus, v)) diff --git a/backend/ent/paymentorder_create.go b/backend/ent/paymentorder_create.go index 03098339..497ba52c 100644 --- a/backend/ent/paymentorder_create.go +++ b/backend/ent/paymentorder_create.go @@ -225,6 +225,20 @@ func (_c *PaymentOrderCreate) SetNillableProviderInstanceID(v *string) *PaymentO return _c } +// SetProviderKey sets the "provider_key" field. +func (_c *PaymentOrderCreate) SetProviderKey(v string) *PaymentOrderCreate { + _c.mutation.SetProviderKey(v) + return _c +} + +// SetNillableProviderKey sets the "provider_key" field if the given value is not nil. +func (_c *PaymentOrderCreate) SetNillableProviderKey(v *string) *PaymentOrderCreate { + if v != nil { + _c.SetProviderKey(*v) + } + return _c +} + // SetStatus sets the "status" field. func (_c *PaymentOrderCreate) SetStatus(v string) *PaymentOrderCreate { _c.mutation.SetStatus(v) @@ -602,6 +616,11 @@ func (_c *PaymentOrderCreate) check() error { return &ValidationError{Name: "provider_instance_id", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.provider_instance_id": %w`, err)} } } + if v, ok := _c.mutation.ProviderKey(); ok { + if err := paymentorder.ProviderKeyValidator(v); err != nil { + return &ValidationError{Name: "provider_key", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.provider_key": %w`, err)} + } + } if _, ok := _c.mutation.Status(); !ok { return &ValidationError{Name: "status", err: errors.New(`ent: missing required field "PaymentOrder.status"`)} } @@ -748,6 +767,10 @@ func (_c *PaymentOrderCreate) createSpec() (*PaymentOrder, *sqlgraph.CreateSpec) _spec.SetField(paymentorder.FieldProviderInstanceID, field.TypeString, value) _node.ProviderInstanceID = &value } + if value, ok := _c.mutation.ProviderKey(); ok { + _spec.SetField(paymentorder.FieldProviderKey, field.TypeString, value) + _node.ProviderKey = &value + } if value, ok := _c.mutation.Status(); ok { _spec.SetField(paymentorder.FieldStatus, field.TypeString, value) _node.Status = value @@ -1201,6 +1224,24 @@ func (u *PaymentOrderUpsert) ClearProviderInstanceID() *PaymentOrderUpsert { return u } +// SetProviderKey sets the "provider_key" field. +func (u *PaymentOrderUpsert) SetProviderKey(v string) *PaymentOrderUpsert { + u.Set(paymentorder.FieldProviderKey, v) + return u +} + +// UpdateProviderKey sets the "provider_key" field to the value that was provided on create. +func (u *PaymentOrderUpsert) UpdateProviderKey() *PaymentOrderUpsert { + u.SetExcluded(paymentorder.FieldProviderKey) + return u +} + +// ClearProviderKey clears the value of the "provider_key" field. +func (u *PaymentOrderUpsert) ClearProviderKey() *PaymentOrderUpsert { + u.SetNull(paymentorder.FieldProviderKey) + return u +} + // SetStatus sets the "status" field. func (u *PaymentOrderUpsert) SetStatus(v string) *PaymentOrderUpsert { u.Set(paymentorder.FieldStatus, v) @@ -1880,6 +1921,27 @@ func (u *PaymentOrderUpsertOne) ClearProviderInstanceID() *PaymentOrderUpsertOne }) } +// SetProviderKey sets the "provider_key" field. +func (u *PaymentOrderUpsertOne) SetProviderKey(v string) *PaymentOrderUpsertOne { + return u.Update(func(s *PaymentOrderUpsert) { + s.SetProviderKey(v) + }) +} + +// UpdateProviderKey sets the "provider_key" field to the value that was provided on create. +func (u *PaymentOrderUpsertOne) UpdateProviderKey() *PaymentOrderUpsertOne { + return u.Update(func(s *PaymentOrderUpsert) { + s.UpdateProviderKey() + }) +} + +// ClearProviderKey clears the value of the "provider_key" field. +func (u *PaymentOrderUpsertOne) ClearProviderKey() *PaymentOrderUpsertOne { + return u.Update(func(s *PaymentOrderUpsert) { + s.ClearProviderKey() + }) +} + // SetStatus sets the "status" field. func (u *PaymentOrderUpsertOne) SetStatus(v string) *PaymentOrderUpsertOne { return u.Update(func(s *PaymentOrderUpsert) { @@ -2770,6 +2832,27 @@ func (u *PaymentOrderUpsertBulk) ClearProviderInstanceID() *PaymentOrderUpsertBu }) } +// SetProviderKey sets the "provider_key" field. +func (u *PaymentOrderUpsertBulk) SetProviderKey(v string) *PaymentOrderUpsertBulk { + return u.Update(func(s *PaymentOrderUpsert) { + s.SetProviderKey(v) + }) +} + +// UpdateProviderKey sets the "provider_key" field to the value that was provided on create. +func (u *PaymentOrderUpsertBulk) UpdateProviderKey() *PaymentOrderUpsertBulk { + return u.Update(func(s *PaymentOrderUpsert) { + s.UpdateProviderKey() + }) +} + +// ClearProviderKey clears the value of the "provider_key" field. +func (u *PaymentOrderUpsertBulk) ClearProviderKey() *PaymentOrderUpsertBulk { + return u.Update(func(s *PaymentOrderUpsert) { + s.ClearProviderKey() + }) +} + // SetStatus sets the "status" field. func (u *PaymentOrderUpsertBulk) SetStatus(v string) *PaymentOrderUpsertBulk { return u.Update(func(s *PaymentOrderUpsert) { diff --git a/backend/ent/paymentorder_update.go b/backend/ent/paymentorder_update.go index 5978fc29..9a901415 100644 --- a/backend/ent/paymentorder_update.go +++ b/backend/ent/paymentorder_update.go @@ -385,6 +385,26 @@ func (_u *PaymentOrderUpdate) ClearProviderInstanceID() *PaymentOrderUpdate { return _u } +// SetProviderKey sets the "provider_key" field. +func (_u *PaymentOrderUpdate) SetProviderKey(v string) *PaymentOrderUpdate { + _u.mutation.SetProviderKey(v) + return _u +} + +// SetNillableProviderKey sets the "provider_key" field if the given value is not nil. +func (_u *PaymentOrderUpdate) SetNillableProviderKey(v *string) *PaymentOrderUpdate { + if v != nil { + _u.SetProviderKey(*v) + } + return _u +} + +// ClearProviderKey clears the value of the "provider_key" field. +func (_u *PaymentOrderUpdate) ClearProviderKey() *PaymentOrderUpdate { + _u.mutation.ClearProviderKey() + return _u +} + // SetStatus sets the "status" field. func (_u *PaymentOrderUpdate) SetStatus(v string) *PaymentOrderUpdate { _u.mutation.SetStatus(v) @@ -776,6 +796,11 @@ func (_u *PaymentOrderUpdate) check() error { return &ValidationError{Name: "provider_instance_id", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.provider_instance_id": %w`, err)} } } + if v, ok := _u.mutation.ProviderKey(); ok { + if err := paymentorder.ProviderKeyValidator(v); err != nil { + return &ValidationError{Name: "provider_key", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.provider_key": %w`, err)} + } + } if v, ok := _u.mutation.Status(); ok { if err := paymentorder.StatusValidator(v); err != nil { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.status": %w`, err)} @@ -910,6 +935,12 @@ func (_u *PaymentOrderUpdate) sqlSave(ctx context.Context) (_node int, err error if _u.mutation.ProviderInstanceIDCleared() { _spec.ClearField(paymentorder.FieldProviderInstanceID, field.TypeString) } + if value, ok := _u.mutation.ProviderKey(); ok { + _spec.SetField(paymentorder.FieldProviderKey, field.TypeString, value) + } + if _u.mutation.ProviderKeyCleared() { + _spec.ClearField(paymentorder.FieldProviderKey, field.TypeString) + } if value, ok := _u.mutation.Status(); ok { _spec.SetField(paymentorder.FieldStatus, field.TypeString, value) } @@ -1399,6 +1430,26 @@ func (_u *PaymentOrderUpdateOne) ClearProviderInstanceID() *PaymentOrderUpdateOn return _u } +// SetProviderKey sets the "provider_key" field. +func (_u *PaymentOrderUpdateOne) SetProviderKey(v string) *PaymentOrderUpdateOne { + _u.mutation.SetProviderKey(v) + return _u +} + +// SetNillableProviderKey sets the "provider_key" field if the given value is not nil. +func (_u *PaymentOrderUpdateOne) SetNillableProviderKey(v *string) *PaymentOrderUpdateOne { + if v != nil { + _u.SetProviderKey(*v) + } + return _u +} + +// ClearProviderKey clears the value of the "provider_key" field. +func (_u *PaymentOrderUpdateOne) ClearProviderKey() *PaymentOrderUpdateOne { + _u.mutation.ClearProviderKey() + return _u +} + // SetStatus sets the "status" field. func (_u *PaymentOrderUpdateOne) SetStatus(v string) *PaymentOrderUpdateOne { _u.mutation.SetStatus(v) @@ -1803,6 +1854,11 @@ func (_u *PaymentOrderUpdateOne) check() error { return &ValidationError{Name: "provider_instance_id", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.provider_instance_id": %w`, err)} } } + if v, ok := _u.mutation.ProviderKey(); ok { + if err := paymentorder.ProviderKeyValidator(v); err != nil { + return &ValidationError{Name: "provider_key", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.provider_key": %w`, err)} + } + } if v, ok := _u.mutation.Status(); ok { if err := paymentorder.StatusValidator(v); err != nil { return &ValidationError{Name: "status", err: fmt.Errorf(`ent: validator failed for field "PaymentOrder.status": %w`, err)} @@ -1954,6 +2010,12 @@ func (_u *PaymentOrderUpdateOne) sqlSave(ctx context.Context) (_node *PaymentOrd if _u.mutation.ProviderInstanceIDCleared() { _spec.ClearField(paymentorder.FieldProviderInstanceID, field.TypeString) } + if value, ok := _u.mutation.ProviderKey(); ok { + _spec.SetField(paymentorder.FieldProviderKey, field.TypeString, value) + } + if _u.mutation.ProviderKeyCleared() { + _spec.ClearField(paymentorder.FieldProviderKey, field.TypeString) + } if value, ok := _u.mutation.Status(); ok { _spec.SetField(paymentorder.FieldStatus, field.TypeString, value) } diff --git a/backend/ent/runtime/runtime.go b/backend/ent/runtime/runtime.go index 268e9ddb..b7118ac9 100644 --- a/backend/ent/runtime/runtime.go +++ b/backend/ent/runtime/runtime.go @@ -723,38 +723,42 @@ func init() { paymentorderDescProviderInstanceID := paymentorderFields[18].Descriptor() // paymentorder.ProviderInstanceIDValidator is a validator for the "provider_instance_id" field. It is called by the builders before save. paymentorder.ProviderInstanceIDValidator = paymentorderDescProviderInstanceID.Validators[0].(func(string) error) + // paymentorderDescProviderKey is the schema descriptor for provider_key field. + paymentorderDescProviderKey := paymentorderFields[19].Descriptor() + // paymentorder.ProviderKeyValidator is a validator for the "provider_key" field. It is called by the builders before save. + paymentorder.ProviderKeyValidator = paymentorderDescProviderKey.Validators[0].(func(string) error) // paymentorderDescStatus is the schema descriptor for status field. - paymentorderDescStatus := paymentorderFields[19].Descriptor() + paymentorderDescStatus := paymentorderFields[20].Descriptor() // paymentorder.DefaultStatus holds the default value on creation for the status field. paymentorder.DefaultStatus = paymentorderDescStatus.Default.(string) // paymentorder.StatusValidator is a validator for the "status" field. It is called by the builders before save. paymentorder.StatusValidator = paymentorderDescStatus.Validators[0].(func(string) error) // paymentorderDescRefundAmount is the schema descriptor for refund_amount field. - paymentorderDescRefundAmount := paymentorderFields[20].Descriptor() + paymentorderDescRefundAmount := paymentorderFields[21].Descriptor() // paymentorder.DefaultRefundAmount holds the default value on creation for the refund_amount field. paymentorder.DefaultRefundAmount = paymentorderDescRefundAmount.Default.(float64) // paymentorderDescForceRefund is the schema descriptor for force_refund field. - paymentorderDescForceRefund := paymentorderFields[23].Descriptor() + paymentorderDescForceRefund := paymentorderFields[24].Descriptor() // paymentorder.DefaultForceRefund holds the default value on creation for the force_refund field. paymentorder.DefaultForceRefund = paymentorderDescForceRefund.Default.(bool) // paymentorderDescRefundRequestedBy is the schema descriptor for refund_requested_by field. - paymentorderDescRefundRequestedBy := paymentorderFields[26].Descriptor() + paymentorderDescRefundRequestedBy := paymentorderFields[27].Descriptor() // paymentorder.RefundRequestedByValidator is a validator for the "refund_requested_by" field. It is called by the builders before save. paymentorder.RefundRequestedByValidator = paymentorderDescRefundRequestedBy.Validators[0].(func(string) error) // paymentorderDescClientIP is the schema descriptor for client_ip field. - paymentorderDescClientIP := paymentorderFields[32].Descriptor() + paymentorderDescClientIP := paymentorderFields[33].Descriptor() // paymentorder.ClientIPValidator is a validator for the "client_ip" field. It is called by the builders before save. paymentorder.ClientIPValidator = paymentorderDescClientIP.Validators[0].(func(string) error) // paymentorderDescSrcHost is the schema descriptor for src_host field. - paymentorderDescSrcHost := paymentorderFields[33].Descriptor() + paymentorderDescSrcHost := paymentorderFields[34].Descriptor() // paymentorder.SrcHostValidator is a validator for the "src_host" field. It is called by the builders before save. paymentorder.SrcHostValidator = paymentorderDescSrcHost.Validators[0].(func(string) error) // paymentorderDescCreatedAt is the schema descriptor for created_at field. - paymentorderDescCreatedAt := paymentorderFields[35].Descriptor() + paymentorderDescCreatedAt := paymentorderFields[36].Descriptor() // paymentorder.DefaultCreatedAt holds the default value on creation for the created_at field. paymentorder.DefaultCreatedAt = paymentorderDescCreatedAt.Default.(func() time.Time) // paymentorderDescUpdatedAt is the schema descriptor for updated_at field. - paymentorderDescUpdatedAt := paymentorderFields[36].Descriptor() + paymentorderDescUpdatedAt := paymentorderFields[37].Descriptor() // paymentorder.DefaultUpdatedAt holds the default value on creation for the updated_at field. paymentorder.DefaultUpdatedAt = paymentorderDescUpdatedAt.Default.(func() time.Time) // paymentorder.UpdateDefaultUpdatedAt holds the default value on update for the updated_at field. diff --git a/backend/ent/schema/payment_order.go b/backend/ent/schema/payment_order.go index a9576d2a..64378de1 100644 --- a/backend/ent/schema/payment_order.go +++ b/backend/ent/schema/payment_order.go @@ -91,6 +91,10 @@ func (PaymentOrder) Fields() []ent.Field { Optional(). Nillable(). MaxLen(64), + field.String("provider_key"). + Optional(). + Nillable(). + MaxLen(30), // 状态 field.String("status"). diff --git a/backend/internal/service/payment_fulfillment.go b/backend/internal/service/payment_fulfillment.go index 519455f0..83bac21d 100644 --- a/backend/internal/service/payment_fulfillment.go +++ b/backend/internal/service/payment_fulfillment.go @@ -45,7 +45,7 @@ func (s *PaymentService) confirmPayment(ctx context.Context, oid int64, tradeNo if inst, instErr := s.getOrderProviderInstance(ctx, o); instErr == nil && inst != nil { instanceProviderKey = inst.ProviderKey } - expectedProviderKey := expectedNotificationProviderKey(s.registry, o.PaymentType, instanceProviderKey) + expectedProviderKey := expectedNotificationProviderKey(s.registry, o.PaymentType, psStringValue(o.ProviderKey), instanceProviderKey) if expectedProviderKey != "" && strings.TrimSpace(pk) != "" && !strings.EqualFold(expectedProviderKey, strings.TrimSpace(pk)) { s.writeAuditLog(ctx, o.ID, "PAYMENT_PROVIDER_MISMATCH", pk, map[string]any{ "expectedProvider": expectedProviderKey, @@ -69,10 +69,13 @@ func (s *PaymentService) confirmPayment(ctx context.Context, oid int64, tradeNo return s.toPaid(ctx, o, tradeNo, paid, pk) } -func expectedNotificationProviderKey(registry *payment.Registry, orderPaymentType string, instanceProviderKey string) string { +func expectedNotificationProviderKey(registry *payment.Registry, orderPaymentType string, orderProviderKey string, instanceProviderKey string) string { if key := strings.TrimSpace(instanceProviderKey); key != "" { return key } + if key := strings.TrimSpace(orderProviderKey); key != "" { + return key + } if registry != nil { if key := strings.TrimSpace(registry.GetProviderKey(payment.PaymentType(orderPaymentType))); key != "" { return key diff --git a/backend/internal/service/payment_fulfillment_test.go b/backend/internal/service/payment_fulfillment_test.go index 4cc00301..712129b0 100644 --- a/backend/internal/service/payment_fulfillment_test.go +++ b/backend/internal/service/payment_fulfillment_test.go @@ -198,7 +198,7 @@ func TestExpectedNotificationProviderKeyPrefersOrderInstanceProvider(t *testing. assert.Equal(t, payment.TypeEasyPay, - expectedNotificationProviderKey(registry, payment.TypeAlipay, payment.TypeEasyPay), + expectedNotificationProviderKey(registry, payment.TypeAlipay, "", payment.TypeEasyPay), ) } @@ -213,7 +213,7 @@ func TestExpectedNotificationProviderKeyUsesRegistryMappingForLegacyOrders(t *te assert.Equal(t, payment.TypeEasyPay, - expectedNotificationProviderKey(registry, payment.TypeAlipay, ""), + expectedNotificationProviderKey(registry, payment.TypeAlipay, "", ""), ) } @@ -222,6 +222,21 @@ func TestExpectedNotificationProviderKeyFallsBackToPaymentType(t *testing.T) { assert.Equal(t, payment.TypeWxpay, - expectedNotificationProviderKey(nil, payment.TypeWxpay, ""), + expectedNotificationProviderKey(nil, payment.TypeWxpay, "", ""), + ) +} + +func TestExpectedNotificationProviderKeyPrefersOrderSnapshotProviderKey(t *testing.T) { + t.Parallel() + + registry := payment.NewRegistry() + registry.Register(paymentFulfillmentTestProvider{ + key: payment.TypeAlipay, + supportedTypes: []payment.PaymentType{payment.TypeAlipay}, + }) + + assert.Equal(t, + payment.TypeEasyPay, + expectedNotificationProviderKey(registry, payment.TypeAlipay, payment.TypeEasyPay, ""), ) } diff --git a/backend/internal/service/payment_order.go b/backend/internal/service/payment_order.go index fa256be7..4b9b1872 100644 --- a/backend/internal/service/payment_order.go +++ b/backend/internal/service/payment_order.go @@ -251,7 +251,13 @@ func (s *PaymentService) invokeProvider(ctx context.Context, order *dbent.Paymen slog.Error("[PaymentService] CreatePayment failed", "provider", sel.ProviderKey, "instance", sel.InstanceID, "error", err) return nil, infraerrors.ServiceUnavailable("PAYMENT_GATEWAY_ERROR", fmt.Sprintf("payment gateway error: %s", err.Error())) } - _, err = s.entClient.PaymentOrder.UpdateOneID(order.ID).SetNillablePaymentTradeNo(psNilIfEmpty(pr.TradeNo)).SetNillablePayURL(psNilIfEmpty(pr.PayURL)).SetNillableQrCode(psNilIfEmpty(pr.QRCode)).SetNillableProviderInstanceID(psNilIfEmpty(sel.InstanceID)).Save(ctx) + _, err = s.entClient.PaymentOrder.UpdateOneID(order.ID). + SetNillablePaymentTradeNo(psNilIfEmpty(pr.TradeNo)). + SetNillablePayURL(psNilIfEmpty(pr.PayURL)). + SetNillableQrCode(psNilIfEmpty(pr.QRCode)). + SetNillableProviderInstanceID(psNilIfEmpty(sel.InstanceID)). + SetNillableProviderKey(psNilIfEmpty(sel.ProviderKey)). + Save(ctx) if err != nil { return nil, fmt.Errorf("update order with payment details: %w", err) } diff --git a/backend/internal/service/payment_order_lifecycle.go b/backend/internal/service/payment_order_lifecycle.go index 80147180..f804eb8b 100644 --- a/backend/internal/service/payment_order_lifecycle.go +++ b/backend/internal/service/payment_order_lifecycle.go @@ -5,6 +5,7 @@ import ( "fmt" "log/slog" "strconv" + "strings" "time" dbent "github.com/Wei-Shaw/sub2api/ent" @@ -241,7 +242,10 @@ func (s *PaymentService) getOrderProvider(ctx context.Context, o *dbent.PaymentO if err == nil { cfg, err := s.loadBalancer.GetInstanceConfig(ctx, instID) if err == nil { - providerKey := s.registry.GetProviderKey(o.PaymentType) + providerKey := strings.TrimSpace(psStringValue(o.ProviderKey)) + if providerKey == "" { + providerKey = s.registry.GetProviderKey(o.PaymentType) + } if providerKey == "" { providerKey = o.PaymentType } @@ -255,3 +259,10 @@ func (s *PaymentService) getOrderProvider(ctx context.Context, o *dbent.PaymentO s.EnsureProviders(ctx) return s.registry.GetProvider(o.PaymentType) } + +func psStringValue(value *string) string { + if value == nil { + return "" + } + return *value +} diff --git a/backend/migrations/112_add_payment_order_provider_key_snapshot.sql b/backend/migrations/112_add_payment_order_provider_key_snapshot.sql new file mode 100644 index 00000000..7ec19ae3 --- /dev/null +++ b/backend/migrations/112_add_payment_order_provider_key_snapshot.sql @@ -0,0 +1,10 @@ +ALTER TABLE payment_orders ADD COLUMN provider_key VARCHAR(30); + +UPDATE payment_orders +SET provider_key = ( + SELECT provider_key + FROM payment_provider_instances + WHERE CAST(id AS TEXT) = payment_orders.provider_instance_id +) +WHERE provider_key IS NULL + AND provider_instance_id IS NOT NULL;