fix: edit existing payment account in binding form
This commit is contained in:
+53
-24
@@ -44,6 +44,7 @@ type Repository interface {
|
||||
PaymentAccounts(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||
PaymentAccountDetails(context.Context, int64, int64) ([]store.PaymentAccount, error)
|
||||
CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
||||
UpdatePaymentAccount(context.Context, int64, store.PaymentAccountCreation) (*store.PaymentAccount, error)
|
||||
PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error)
|
||||
ActiveWithdrawalChannel(context.Context, int64, []int) (*store.WithdrawalChannel, error)
|
||||
WithdrawalWallet(context.Context, int64, int64) (*store.WithdrawalWallet, error)
|
||||
@@ -67,6 +68,7 @@ type Service struct {
|
||||
}
|
||||
|
||||
type PaymentAccountInput struct {
|
||||
ID int64 `json:"id"`
|
||||
BankName string `json:"bank_name"`
|
||||
HolderName string `json:"holder_name"`
|
||||
IdentityNo string `json:"identity_no"`
|
||||
@@ -94,17 +96,19 @@ type PaymentAccount struct {
|
||||
}
|
||||
|
||||
type PaymentAccountDetail struct {
|
||||
ID int64 `json:"id"`
|
||||
BankName string `json:"bank_name"`
|
||||
HolderName string `json:"holder_name"`
|
||||
IdentityNo string `json:"identity_no"`
|
||||
AccountNo string `json:"account_no"`
|
||||
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||
WechatQRURL string `json:"wechat_qr_url"`
|
||||
AlipayQRURL string `json:"alipay_qr_url"`
|
||||
VerifyStatus string `json:"verify_status"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Masked bool `json:"masked"`
|
||||
ID int64 `json:"id"`
|
||||
BankName string `json:"bank_name"`
|
||||
HolderName string `json:"holder_name"`
|
||||
IdentityNo string `json:"identity_no"`
|
||||
AccountNo string `json:"account_no"`
|
||||
DigitalRMBCode string `json:"digital_rmb_code"`
|
||||
WechatQRURL string `json:"wechat_qr_url"`
|
||||
AlipayQRURL string `json:"alipay_qr_url"`
|
||||
WechatQRAttachmentID int64 `json:"wechat_qr_attachment_id"`
|
||||
AlipayQRAttachmentID int64 `json:"alipay_qr_attachment_id"`
|
||||
VerifyStatus string `json:"verify_status"`
|
||||
IsDefault bool `json:"is_default"`
|
||||
Masked bool `json:"masked"`
|
||||
}
|
||||
|
||||
type QuoteInput struct {
|
||||
@@ -335,6 +339,7 @@ func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID
|
||||
ID: row.ID, BankName: row.BankName, HolderName: holderName,
|
||||
IdentityNo: identityNo, AccountNo: accountNo, DigitalRMBCode: digitalRMBCode,
|
||||
WechatQRURL: row.WechatQRURL, AlipayQRURL: row.AlipayQRURL,
|
||||
WechatQRAttachmentID: row.WechatQRAttachmentID, AlipayQRAttachmentID: row.AlipayQRAttachmentID,
|
||||
VerifyStatus: verifyStatusName(row.VerifyStatus), IsDefault: row.IsDefault == 1, Masked: masked,
|
||||
})
|
||||
}
|
||||
@@ -342,34 +347,63 @@ func (s *Service) PaymentAccountDetails(ctx context.Context, tenantID, memberID
|
||||
}
|
||||
|
||||
func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
||||
data, err := s.paymentAccountCreation(tenantID, memberID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, err := s.repository.CreatePaymentAccount(ctx, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := paymentAccountFromStore(*row)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Service) UpdatePaymentAccount(ctx context.Context, tenantID, memberID int64, input PaymentAccountInput) (*PaymentAccount, error) {
|
||||
if input.ID <= 0 {
|
||||
return nil, ErrAccountFieldsInvalid
|
||||
}
|
||||
data, err := s.paymentAccountCreation(tenantID, memberID, input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
row, err := s.repository.UpdatePaymentAccount(ctx, input.ID, data)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := paymentAccountFromStore(*row)
|
||||
return &result, nil
|
||||
}
|
||||
|
||||
func (s *Service) paymentAccountCreation(tenantID, memberID int64, input PaymentAccountInput) (store.PaymentAccountCreation, error) {
|
||||
if !s.ready {
|
||||
return nil, ErrUnavailable
|
||||
return store.PaymentAccountCreation{}, ErrUnavailable
|
||||
}
|
||||
if err := normalizePaymentAccount(&input); err != nil {
|
||||
return nil, err
|
||||
return store.PaymentAccountCreation{}, err
|
||||
}
|
||||
holderCipher, err := s.encrypt(input.HolderName)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return store.PaymentAccountCreation{}, err
|
||||
}
|
||||
identityNoCipher, err := s.encrypt(input.IdentityNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return store.PaymentAccountCreation{}, err
|
||||
}
|
||||
accountCipher, err := s.encrypt(input.AccountNo)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return store.PaymentAccountCreation{}, err
|
||||
}
|
||||
digitalRMBCodeCipher, digitalRMBCodeHash, digitalRMBCodeMask := "", "", ""
|
||||
if input.DigitalRMBCode != "" {
|
||||
digitalRMBCodeCipher, err = s.encrypt(input.DigitalRMBCode)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return store.PaymentAccountCreation{}, err
|
||||
}
|
||||
digitalRMBCodeHash = s.lookupHash(input.DigitalRMBCode)
|
||||
digitalRMBCodeMask = maskAccount(input.DigitalRMBCode)
|
||||
}
|
||||
row, err := s.repository.CreatePaymentAccount(ctx, store.PaymentAccountCreation{
|
||||
return store.PaymentAccountCreation{
|
||||
TenantID: tenantID, MemberID: memberID, AccountType: 1, BankCode: "BANK", BankName: input.BankName,
|
||||
HolderCipher: holderCipher, HolderMask: maskName(input.HolderName),
|
||||
IdentityNoCipher: identityNoCipher, IdentityNoHash: s.lookupHash(input.IdentityNo),
|
||||
@@ -379,12 +413,7 @@ func (s *Service) CreatePaymentAccount(ctx context.Context, tenantID, memberID i
|
||||
DigitalRMBCodeMask: digitalRMBCodeMask,
|
||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
result := paymentAccountFromStore(*row)
|
||||
return &result, nil
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *Service) Quote(ctx context.Context, tenantID, memberID int64, input QuoteInput) (*Quote, error) {
|
||||
|
||||
@@ -27,7 +27,9 @@ type fakeRepository struct {
|
||||
|
||||
type paymentAccountRepository struct {
|
||||
fakeRepository
|
||||
created store.PaymentAccountCreation
|
||||
created store.PaymentAccountCreation
|
||||
updated store.PaymentAccountCreation
|
||||
updatedID int64
|
||||
}
|
||||
|
||||
func (f *paymentAccountRepository) CreatePaymentAccount(_ context.Context, input store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||
@@ -35,6 +37,12 @@ func (f *paymentAccountRepository) CreatePaymentAccount(_ context.Context, input
|
||||
return &store.PaymentAccount{ID: 1, AccountType: input.AccountType}, nil
|
||||
}
|
||||
|
||||
func (f *paymentAccountRepository) UpdatePaymentAccount(_ context.Context, id int64, input store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||
f.updatedID = id
|
||||
f.updated = input
|
||||
return &store.PaymentAccount{ID: id, AccountType: input.AccountType, BankName: input.BankName}, nil
|
||||
}
|
||||
|
||||
func (f *fakeRepository) CurrentSiteConfig(context.Context, int64) (json.RawMessage, error) {
|
||||
return f.config, nil
|
||||
}
|
||||
@@ -50,6 +58,9 @@ func (f *fakeRepository) PaymentAccountDetails(context.Context, int64, int64) ([
|
||||
func (f *fakeRepository) CreatePaymentAccount(context.Context, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
func (f *fakeRepository) UpdatePaymentAccount(context.Context, int64, store.PaymentAccountCreation) (*store.PaymentAccount, error) {
|
||||
return nil, errors.New("not implemented")
|
||||
}
|
||||
func (f *fakeRepository) PaymentAccountForWithdrawal(context.Context, int64, int64, int64) (*store.PaymentAccount, error) {
|
||||
return f.account, nil
|
||||
}
|
||||
@@ -155,7 +166,8 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
|
||||
repository.details = []store.PaymentAccount{{
|
||||
ID: 7, BankName: "中国工商银行", HolderCipher: holder, IdentityNoCipher: identity,
|
||||
AccountNoCipher: account, DigitalRMBCodeCipher: digital,
|
||||
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png", VerifyStatus: 2,
|
||||
WechatQRURL: "/wechat.png", AlipayQRURL: "/alipay.png",
|
||||
WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9, VerifyStatus: 2,
|
||||
}}
|
||||
items, err := service.PaymentAccountDetails(context.Background(), 1, 2)
|
||||
if err != nil {
|
||||
@@ -163,11 +175,34 @@ func TestPaymentAccountDetailsReturnsOriginalFields(t *testing.T) {
|
||||
}
|
||||
if len(items) != 1 || items[0].HolderName != "张三" || items[0].IdentityNo != "11010119900101123X" ||
|
||||
items[0].AccountNo != "6222000000001234" || items[0].DigitalRMBCode != "123456" ||
|
||||
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" {
|
||||
items[0].WechatQRURL != "/wechat.png" || items[0].AlipayQRURL != "/alipay.png" ||
|
||||
items[0].WechatQRAttachmentID != 8 || items[0].AlipayQRAttachmentID != 9 {
|
||||
t.Fatalf("unexpected account details: %#v", items)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUpdatePaymentAccountEncryptsEditedFields(t *testing.T) {
|
||||
repository := &paymentAccountRepository{}
|
||||
service := New(repository, &redis.Client{}, config.AuthConfig{
|
||||
FinanceMasterKey: "test-finance-master-key-with-at-least-32-characters",
|
||||
})
|
||||
result, err := service.UpdatePaymentAccount(context.Background(), 1, 2, PaymentAccountInput{
|
||||
ID: 7, BankName: "中国工商银行", HolderName: "李四", IdentityNo: "11010119900101123X",
|
||||
AccountNo: "6222000000001234", WechatQRAttachmentID: 8, AlipayQRAttachmentID: 9,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if result.ID != 7 || repository.updatedID != 7 || repository.updated.TenantID != 1 || repository.updated.MemberID != 2 ||
|
||||
repository.updated.WechatQRAttachmentID != 8 || repository.updated.AlipayQRAttachmentID != 9 ||
|
||||
repository.updated.DigitalRMBCodeCipher != "" {
|
||||
t.Fatalf("unexpected updated account: result=%#v data=%#v", result, repository.updated)
|
||||
}
|
||||
if holder, err := service.decrypt(repository.updated.HolderCipher); err != nil || holder != "李四" {
|
||||
t.Fatalf("edited holder was not encrypted correctly: %q, %v", holder, err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPaymentAccountDetailsFallsBackToMasksWhenHistoricalKeyIsUnavailable(t *testing.T) {
|
||||
repository := &fakeRepository{details: []store.PaymentAccount{{
|
||||
ID: 8, BankName: "中国银行", HolderCipher: "legacy-holder", HolderMask: "张*",
|
||||
|
||||
@@ -12,8 +12,8 @@ import (
|
||||
)
|
||||
|
||||
func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodPost {
|
||||
methodNotAllowed(w, r, http.MethodGet, http.MethodPost)
|
||||
if r.Method != http.MethodGet && r.Method != http.MethodPost && r.Method != http.MethodPut {
|
||||
methodNotAllowed(w, r, http.MethodGet, http.MethodPost, http.MethodPut)
|
||||
return
|
||||
}
|
||||
principal, ok := a.authenticateFinanceMember(w, r)
|
||||
@@ -33,7 +33,13 @@ func (a *API) paymentAccounts(w http.ResponseWriter, r *http.Request) {
|
||||
if !decodeBody(w, r, &body) {
|
||||
return
|
||||
}
|
||||
result, err := a.finance.CreatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body)
|
||||
var result *finance.PaymentAccount
|
||||
var err error
|
||||
if r.Method == http.MethodPut {
|
||||
result, err = a.finance.UpdatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body)
|
||||
} else {
|
||||
result, err = a.finance.CreatePaymentAccount(r.Context(), principal.TenantID, principal.MemberID, body)
|
||||
}
|
||||
if err != nil {
|
||||
writeFinanceError(w, r, err)
|
||||
return
|
||||
|
||||
@@ -251,6 +251,59 @@ func (s *Store) CreatePaymentAccount(ctx context.Context, input PaymentAccountCr
|
||||
VerifyStatus: 2, IsDefault: isDefault}, nil
|
||||
}
|
||||
|
||||
func (s *Store) UpdatePaymentAccount(ctx context.Context, accountID int64, input PaymentAccountCreation) (*PaymentAccount, error) {
|
||||
tx, err := s.db.BeginTxx(ctx, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer tx.Rollback()
|
||||
var isDefault int
|
||||
err = tx.GetContext(ctx, &isDefault, `SELECT is_default FROM hg_app_member_payment_account
|
||||
WHERE id=? AND tenant_id=? AND member_id=? AND deleted_at IS NULL LIMIT 1 FOR UPDATE`,
|
||||
accountID, input.TenantID, input.MemberID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return nil, ErrPaymentAccountInvalid
|
||||
}
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
var attachmentCount int
|
||||
err = tx.GetContext(ctx, &attachmentCount, `SELECT COUNT(*) FROM hg_sys_attachment
|
||||
WHERE id IN (?,?) AND app_id='olons_server' AND member_id=? AND status=1 AND mime_type LIKE 'image/%'`,
|
||||
input.WechatQRAttachmentID, input.AlipayQRAttachmentID, input.MemberID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if attachmentCount != 2 {
|
||||
return nil, ErrAttachmentInvalid
|
||||
}
|
||||
_, err = tx.ExecContext(ctx, `UPDATE hg_app_member_payment_account SET
|
||||
bank_name=?,holder_cipher=?,holder_mask=?,identity_no_cipher=?,identity_no_hash=?,identity_no_mask=?,
|
||||
account_no_cipher=?,account_no_hash=?,account_no_mask=?,digital_rmb_code_cipher=?,
|
||||
digital_rmb_code_hash=?,digital_rmb_code_mask=?,wechat_qr_attachment_id=?,alipay_qr_attachment_id=?,
|
||||
verify_status=2,updated_by=?,updated_at=NOW() WHERE id=? AND tenant_id=? AND member_id=? AND deleted_at IS NULL`,
|
||||
input.BankName, input.HolderCipher, input.HolderMask, input.IdentityNoCipher, input.IdentityNoHash,
|
||||
input.IdentityNoMask, input.AccountNoCipher, input.AccountNoHash, input.AccountNoMask,
|
||||
input.DigitalRMBCodeCipher, input.DigitalRMBCodeHash, input.DigitalRMBCodeMask,
|
||||
input.WechatQRAttachmentID, input.AlipayQRAttachmentID, input.MemberID,
|
||||
accountID, input.TenantID, input.MemberID)
|
||||
if err != nil {
|
||||
if isDuplicate(err) {
|
||||
return nil, ErrPaymentAccountExists
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
if err = tx.Commit(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &PaymentAccount{ID: accountID, AccountType: input.AccountType, BankCode: input.BankCode,
|
||||
BankName: input.BankName, HolderMask: input.HolderMask, IdentityNoMask: input.IdentityNoMask,
|
||||
AccountNoMask: input.AccountNoMask, DigitalRMBCodeMask: input.DigitalRMBCodeMask,
|
||||
WechatQRAttachmentID: input.WechatQRAttachmentID,
|
||||
AlipayQRAttachmentID: input.AlipayQRAttachmentID,
|
||||
VerifyStatus: 2, IsDefault: isDefault}, nil
|
||||
}
|
||||
|
||||
func nullableString(value string) any {
|
||||
if value == "" {
|
||||
return nil
|
||||
|
||||
Reference in New Issue
Block a user