From b8e25ec5b09f1ce0418a28bca725e334cb5f7bd6 Mon Sep 17 00:00:00 2001 From: x1m3 Date: Fri, 3 Jan 2025 12:36:49 +0100 Subject: [PATCH] feat: payment_options.name field is unique --- api/api.yaml | 2 ++ internal/api/api.gen.go | 9 +++++ internal/api/payment.go | 3 ++ internal/api/payment_test.go | 35 ++++++++++++++++--- ...1030640326_payment_options_name_unique.sql | 21 +++++++++++ internal/repositories/payment.go | 7 ++++ internal/repositories/payment_test.go | 13 +++---- 7 files changed, 79 insertions(+), 11 deletions(-) create mode 100644 internal/db/schema/migrations/202501030640326_payment_options_name_unique.sql diff --git a/api/api.yaml b/api/api.yaml index eeeafc59..de4a8078 100644 --- a/api/api.yaml +++ b/api/api.yaml @@ -1897,6 +1897,8 @@ paths: $ref: '#/components/responses/400' '401': $ref: '#/components/responses/401' + '409': + $ref: '#/components/responses/409' '500': $ref: '#/components/responses/500' diff --git a/internal/api/api.gen.go b/internal/api/api.gen.go index 99caa072..f40b213e 100644 --- a/internal/api/api.gen.go +++ b/internal/api/api.gen.go @@ -6301,6 +6301,15 @@ func (response CreatePaymentOption401JSONResponse) VisitCreatePaymentOptionRespo return json.NewEncoder(w).Encode(response) } +type CreatePaymentOption409JSONResponse struct{ N409JSONResponse } + +func (response CreatePaymentOption409JSONResponse) VisitCreatePaymentOptionResponse(w http.ResponseWriter) error { + w.Header().Set("Content-Type", "application/json") + w.WriteHeader(409) + + return json.NewEncoder(w).Encode(response) +} + type CreatePaymentOption500JSONResponse struct{ N500JSONResponse } func (response CreatePaymentOption500JSONResponse) VisitCreatePaymentOptionResponse(w http.ResponseWriter) error { diff --git a/internal/api/payment.go b/internal/api/payment.go index 41656aa2..6a0d0f46 100644 --- a/internal/api/payment.go +++ b/internal/api/payment.go @@ -62,6 +62,9 @@ func (s *Server) CreatePaymentOption(ctx context.Context, request CreatePaymentO if errors.Is(err, repositories.ErrIdentityNotFound) { return CreatePaymentOption400JSONResponse{N400JSONResponse{Message: "invalid issuer did"}}, nil } + if errors.Is(err, repositories.ErrPaymentOptionAlreadyExists) { + return CreatePaymentOption409JSONResponse{N409JSONResponse{Message: "payment option name already exists"}}, nil + } return CreatePaymentOption500JSONResponse{N500JSONResponse{Message: fmt.Sprintf("can't create payment-option: <%s>", err.Error())}}, nil } return CreatePaymentOption201JSONResponse{Id: id.String()}, nil diff --git a/internal/api/payment_test.go b/internal/api/payment_test.go index 2e8bdf9b..b25fd31c 100644 --- a/internal/api/payment_test.go +++ b/internal/api/payment_test.go @@ -84,6 +84,14 @@ func TestServer_CreatePaymentOption(t *testing.T) { require.NoError(t, json.Unmarshal([]byte(paymentOptionConfigurationTesting), &config)) + _, err = server.Services.payments.CreatePaymentOption( + ctx, + issuerDID, + "duplicated name", + "Payment Option explanation", + &domain.PaymentOptionConfig{}) + require.NoError(t, err) + type expected struct { httpCode int msg string @@ -103,7 +111,7 @@ func TestServer_CreatePaymentOption(t *testing.T) { body: CreatePaymentOptionJSONRequestBody{ PaymentOptions: config, Description: "Payment Option explanation", - Name: "1 POL Payment", + Name: "3 POL Payment", }, expected: expected{ httpCode: http.StatusUnauthorized, @@ -117,7 +125,7 @@ func TestServer_CreatePaymentOption(t *testing.T) { body: CreatePaymentOptionJSONRequestBody{ PaymentOptions: config, Description: "Payment Option explanation", - Name: "1 POL Payment", + Name: "4 POL Payment", }, expected: expected{ httpCode: http.StatusCreated, @@ -130,13 +138,27 @@ func TestServer_CreatePaymentOption(t *testing.T) { body: CreatePaymentOptionJSONRequestBody{ PaymentOptions: config, Description: "Payment Option explanation", - Name: "1 POL Payment", + Name: "5 POL Payment", }, expected: expected{ httpCode: http.StatusBadRequest, msg: "invalid issuer did", }, }, + { + name: "Duplicated name", + auth: authOk, + issuerDID: *issuerDID, + body: CreatePaymentOptionJSONRequestBody{ + PaymentOptions: config, + Description: "Payment Option explanation", + Name: "duplicated name", + }, + expected: expected{ + httpCode: http.StatusConflict, + msg: "payment option name already exists", + }, + }, } { t.Run(tc.name, func(t *testing.T) { rr := httptest.NewRecorder() @@ -157,7 +179,10 @@ func TestServer_CreatePaymentOption(t *testing.T) { var response CreatePaymentOption400JSONResponse require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) assert.Equal(t, tc.expected.msg, response.Message) - + case http.StatusConflict: + var response CreatePaymentOption409JSONResponse + require.NoError(t, json.Unmarshal(rr.Body.Bytes(), &response)) + assert.Equal(t, tc.expected.msg, response.Message) } }) } @@ -422,7 +447,7 @@ func TestServer_DeletePaymentOption(t *testing.T) { otherDID, err := w3c.ParseDID("did:polygonid:polygon:amoy:2qRYvPBNBTkPaHk1mKBkcLTequfAdsHzXv549ktnL5") require.NoError(t, err) - optionID, err := server.Services.payments.CreatePaymentOption(ctx, issuerDID, "1 POL Payment", "Payment Option explanation", &config) + optionID, err := server.Services.payments.CreatePaymentOption(ctx, issuerDID, "10 POL Payment", "Payment Option explanation", &config) require.NoError(t, err) type expected struct { httpCode int diff --git a/internal/db/schema/migrations/202501030640326_payment_options_name_unique.sql b/internal/db/schema/migrations/202501030640326_payment_options_name_unique.sql new file mode 100644 index 00000000..b62adc5a --- /dev/null +++ b/internal/db/schema/migrations/202501030640326_payment_options_name_unique.sql @@ -0,0 +1,21 @@ +-- +goose Up +-- +goose StatementBegin + +-- Update the name column with unique names +WITH duplicates AS (SELECT id, name, ROW_NUMBER() OVER (PARTITION BY name ORDER BY id) AS row_num + FROM payment_options) +UPDATE payment_options +SET name =payment_options.name || '_' || duplicates.row_num - 1 +FROM duplicates +WHERE payment_options.id = duplicates.id + AND duplicates.row_num > 1; + +ALTER TABLE payment_options + ADD CONSTRAINT payment_options_name_unique UNIQUE (name); +-- +goose StatementEnd + +-- +goose Down +-- +goose StatementBegin +ALTER TABLE payment_options + DROP CONSTRAINT payment_options_name_unique; +-- +goose StatementEnd \ No newline at end of file diff --git a/internal/repositories/payment.go b/internal/repositories/payment.go index 85edcbb2..4b4e8477 100644 --- a/internal/repositories/payment.go +++ b/internal/repositories/payment.go @@ -20,6 +20,9 @@ import ( // ErrPaymentOptionDoesNotExists error var ErrPaymentOptionDoesNotExists = errors.New("payment option not found") +// ErrPaymentOptionAlreadyExists error +var ErrPaymentOptionAlreadyExists = errors.New("payment option already exists") + // ErrPaymentRequestDoesNotExists error var ErrPaymentRequestDoesNotExists = errors.New("payment request not found") @@ -289,6 +292,10 @@ VALUES ($1, $2, $3, $4, $5, $6, $7); if strings.Contains(err.Error(), "violates foreign key constraint") { return uuid.Nil, ErrIdentityNotFound } + if strings.Contains(err.Error(), "violates unique constraint") { + return uuid.Nil, ErrPaymentOptionAlreadyExists + } + return uuid.Nil, err } return opt.ID, nil } diff --git a/internal/repositories/payment_test.go b/internal/repositories/payment_test.go index e2c28986..f5b3bb2b 100644 --- a/internal/repositories/payment_test.go +++ b/internal/repositories/payment_test.go @@ -139,7 +139,8 @@ func TestPayment_GetPaymentOptionByID(t *testing.T) { fixture.CreateIdentity(t, &domain.Identity{Identifier: issuerID.String()}) repo := NewPayment(*storage) - id, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name", "description", &paymentOptionConfig)) + paymentOption := domain.NewPaymentOption(*issuerID, "payment option name "+uuid.NewString(), "description", &paymentOptionConfig) + id, err := repo.SavePaymentOption(ctx, paymentOption) assert.NoError(t, err) assert.NotEqual(t, uuid.Nil, id) @@ -147,7 +148,7 @@ func TestPayment_GetPaymentOptionByID(t *testing.T) { opt, err := repo.GetPaymentOptionByID(ctx, issuerID, id) assert.NoError(t, err) assert.Equal(t, id, opt.ID) - assert.Equal(t, "name", opt.Name) + assert.Equal(t, paymentOption.Name, opt.Name) assert.Equal(t, "description", opt.Description) assert.Equal(t, paymentOptionConfig, opt.Config) }) @@ -166,7 +167,7 @@ func TestPayment_DeletePaymentOption(t *testing.T) { fixture.CreateIdentity(t, &domain.Identity{Identifier: issuerID.String()}) repo := NewPayment(*storage) - id, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name", "description", &domain.PaymentOptionConfig{})) + id, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name"+uuid.NewString(), "description", &domain.PaymentOptionConfig{})) assert.NoError(t, err) assert.NotEqual(t, uuid.Nil, id) @@ -190,7 +191,7 @@ func TestPayment_SavePaymentRequest(t *testing.T) { fixture.CreateIdentity(t, &domain.Identity{Identifier: issuerID.String()}) repo := NewPayment(*storage) - payymentOptionID, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name", "description", &domain.PaymentOptionConfig{})) + payymentOptionID, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name"+uuid.NewString(), "description", &domain.PaymentOptionConfig{})) require.NoError(t, err) t.Run("Save payment to not existing payment option id", func(t *testing.T) { @@ -266,7 +267,7 @@ func TestPayment_GetPaymentRequestByID(t *testing.T) { require.NoError(t, err) fixture.CreateIdentity(t, &domain.Identity{Identifier: issuerID.String()}) - paymentOptionID, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name", "description", &domain.PaymentOptionConfig{})) + paymentOptionID, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name"+uuid.NewString(), "description", &domain.PaymentOptionConfig{})) require.NoError(t, err) expected := fixture.CreatePaymentRequest(t, *issuerID, *issuerID, paymentOptionID, 10) @@ -298,7 +299,7 @@ func TestPayment_GetPaymentRequestItem(t *testing.T) { fixture.CreateIdentity(t, &domain.Identity{Identifier: issuerID.String()}) repo := NewPayment(*storage) - payymentOptionID, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name", "description", &domain.PaymentOptionConfig{})) + payymentOptionID, err := repo.SavePaymentOption(ctx, domain.NewPaymentOption(*issuerID, "name"+uuid.NewString(), "description", &domain.PaymentOptionConfig{})) require.NoError(t, err) expected := fixture.CreatePaymentRequest(t, *issuerID, *issuerID, payymentOptionID, 10)