From f3f32a66117504745f5422f41d5d1c0e8783a3b4 Mon Sep 17 00:00:00 2001 From: Rowan Seymour Date: Tue, 12 Nov 2024 16:31:06 -0500 Subject: [PATCH] Change transfer_airtime to fail when no exact matching amount --- flows/actions/testdata/transfer_airtime.json | 8 ++-- flows/events/airtime_transferred.go | 35 ++++++++-------- flows/events/base_test.go | 16 ++++---- flows/services.go | 13 +++--- services/airtime/dtone/service.go | 40 +++++++------------ services/airtime/dtone/service_test.go | 27 ++++++++----- test/engine.go | 11 +++-- test/testdata/runner/airtime.json | 2 +- .../airtime.test_successful_transfer.json | 6 +-- 9 files changed, 70 insertions(+), 88 deletions(-) diff --git a/flows/actions/testdata/transfer_airtime.json b/flows/actions/testdata/transfer_airtime.json index 9f8e63a77..923dab0c2 100644 --- a/flows/actions/testdata/transfer_airtime.json +++ b/flows/actions/testdata/transfer_airtime.json @@ -187,7 +187,7 @@ "uuid": "ad154980-7bf7-4ab8-8728-545fd6378912", "amounts": { "RWF": 500, - "USD": 3.5 + "USD": 3 }, "result_name": "Reward Transfer" }, @@ -201,8 +201,7 @@ "sender": "tel:+17036975131", "recipient": "tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123", "currency": "USD", - "desired_amount": 3.5, - "actual_amount": 3, + "amount": 3, "http_logs": [ { "url": "https://dvs-api.dtone.com/v1/lookup/mobile-number", @@ -301,8 +300,7 @@ "sender": "tel:+17036975131", "recipient": "tel:+12065551212?channel=57f1078f-88aa-46f4-a59a-948a5739c03d&id=123", "currency": "", - "desired_amount": 0, - "actual_amount": 0, + "amount": 0, "http_logs": [ { "url": "https://dvs-api.dtone.com/v1/lookup/mobile-number", diff --git a/flows/events/airtime_transferred.go b/flows/events/airtime_transferred.go index f243c4ec6..50c7ced5c 100644 --- a/flows/events/airtime_transferred.go +++ b/flows/events/airtime_transferred.go @@ -23,8 +23,7 @@ const TypeAirtimeTransferred string = "airtime_transferred" // "sender": "tel:4748", // "recipient": "tel:+1242563637", // "currency": "RWF", -// "desired_amount": 120, -// "actual_amount": 100, +// "amount": 100, // "http_logs": [ // { // "url": "https://dvs-api.dtone.com/v1/sync/transactions", @@ -41,27 +40,25 @@ const TypeAirtimeTransferred string = "airtime_transferred" type AirtimeTransferredEvent struct { BaseEvent - TransferUUID flows.AirtimeTransferUUID `json:"transfer_uuid"` - ExternalID string `json:"external_id"` - Sender urns.URN `json:"sender"` - Recipient urns.URN `json:"recipient"` - Currency string `json:"currency"` - DesiredAmount decimal.Decimal `json:"desired_amount"` - ActualAmount decimal.Decimal `json:"actual_amount"` - HTTPLogs []*flows.HTTPLog `json:"http_logs"` + TransferUUID flows.AirtimeTransferUUID `json:"transfer_uuid"` + ExternalID string `json:"external_id"` + Sender urns.URN `json:"sender"` + Recipient urns.URN `json:"recipient"` + Currency string `json:"currency"` + Amount decimal.Decimal `json:"amount"` + HTTPLogs []*flows.HTTPLog `json:"http_logs"` } // NewAirtimeTransferred creates a new airtime transferred event func NewAirtimeTransferred(t *flows.AirtimeTransfer, httpLogs []*flows.HTTPLog) *AirtimeTransferredEvent { return &AirtimeTransferredEvent{ - BaseEvent: NewBaseEvent(TypeAirtimeTransferred), - TransferUUID: t.UUID, - ExternalID: t.ExternalID, - Sender: t.Sender, - Recipient: t.Recipient, - Currency: t.Currency, - DesiredAmount: t.DesiredAmount, - ActualAmount: t.ActualAmount, - HTTPLogs: httpLogs, + BaseEvent: NewBaseEvent(TypeAirtimeTransferred), + TransferUUID: t.UUID, + ExternalID: t.ExternalID, + Sender: t.Sender, + Recipient: t.Recipient, + Currency: t.Currency, + Amount: t.Amount, + HTTPLogs: httpLogs, } } diff --git a/flows/events/base_test.go b/flows/events/base_test.go index 5b62920d7..393b9057a 100644 --- a/flows/events/base_test.go +++ b/flows/events/base_test.go @@ -57,13 +57,12 @@ func TestEventMarshaling(t *testing.T) { { events.NewAirtimeTransferred( &flows.AirtimeTransfer{ - UUID: "4c2d9b7a-e02c-4e6a-ab18-06df4cb5666d", - ExternalID: "98765432", - Sender: urns.URN("tel:+593979099111"), - Recipient: urns.URN("tel:+593979099222"), - Currency: "USD", - DesiredAmount: decimal.RequireFromString("1.20"), - ActualAmount: decimal.RequireFromString("1.00"), + UUID: "4c2d9b7a-e02c-4e6a-ab18-06df4cb5666d", + ExternalID: "98765432", + Sender: urns.URN("tel:+593979099111"), + Recipient: urns.URN("tel:+593979099222"), + Currency: "USD", + Amount: decimal.RequireFromString("1.00"), }, []*flows.HTTPLog{ { @@ -82,10 +81,9 @@ func TestEventMarshaling(t *testing.T) { }, ), `{ - "actual_amount": 1, + "amount": 1, "created_on": "2018-10-18T14:20:30.000123456Z", "currency": "USD", - "desired_amount": 1.2, "external_id": "98765432", "http_logs": [ { diff --git a/flows/services.go b/flows/services.go index f8fa6844b..77fd1d4bc 100644 --- a/flows/services.go +++ b/flows/services.go @@ -157,13 +157,12 @@ const ( // AirtimeTransfer is the result of an attempted airtime transfer type AirtimeTransfer struct { - UUID AirtimeTransferUUID - ExternalID string - Sender urns.URN - Recipient urns.URN - Currency string - DesiredAmount decimal.Decimal - ActualAmount decimal.Decimal + UUID AirtimeTransferUUID + ExternalID string + Sender urns.URN + Recipient urns.URN + Currency string + Amount decimal.Decimal } // AirtimeService provides airtime functionality to the engine diff --git a/services/airtime/dtone/service.go b/services/airtime/dtone/service.go index 3cca2c9c8..46bc66e59 100644 --- a/services/airtime/dtone/service.go +++ b/services/airtime/dtone/service.go @@ -29,12 +29,11 @@ func NewService(httpClient *http.Client, httpRetries *httpx.RetryConfig, key, se func (s *service) Transfer(sender urns.URN, recipient urns.URN, amounts map[string]decimal.Decimal, logHTTP flows.HTTPLogCallback) (*flows.AirtimeTransfer, error) { transfer := &flows.AirtimeTransfer{ - UUID: flows.AirtimeTransferUUID(uuids.NewV4()), - Sender: sender, - Recipient: recipient, - Currency: "", - DesiredAmount: decimal.Zero, - ActualAmount: decimal.Zero, + UUID: flows.AirtimeTransferUUID(uuids.NewV4()), + Sender: sender, + Recipient: recipient, + Currency: "", + Amount: decimal.Zero, } recipientPhoneNumber := recipient.Path() if !strings.HasPrefix(recipientPhoneNumber, "+") { @@ -70,34 +69,24 @@ func (s *service) Transfer(sender urns.URN, recipient urns.URN, amounts map[stri return transfer, fmt.Errorf("product fetch failed: %w", err) } - // closest product for each currency we have a desired amount for - closestProducts := make(map[string]*Product, len(amounts)) - + // find a matching product in any currency we have a desired amount for + var product *Product for currency, desiredAmount := range amounts { - for _, product := range products { - if product.Destination.Unit == currency { - closest := closestProducts[currency] - prodAmount := product.Destination.Amount - - if (closest == nil || prodAmount.GreaterThan(closest.Destination.Amount)) && prodAmount.LessThanOrEqual(desiredAmount) { - closestProducts[currency] = product + for _, p := range products { + if p.Destination.Unit == currency { + if p.Destination.Amount.Equal(desiredAmount) { + product = p + break } } } } - if len(closestProducts) == 0 { + if product == nil { return transfer, fmt.Errorf("unable to find a suitable product for operator '%s'", operator.Name) } - // it's possible we have more than one supported currency/product.. use any - var product *Product - for i := range closestProducts { - product = closestProducts[i] - break - } - transfer.Currency = product.Destination.Unit - transfer.DesiredAmount = amounts[transfer.Currency] + transfer.Amount = product.Destination.Amount // request asynchronous confirmed transaction for this product tx, trace, err := s.client.TransactionAsync(string(transfer.UUID), product.ID, recipientPhoneNumber) @@ -113,7 +102,6 @@ func (s *service) Transfer(sender urns.URN, recipient urns.URN, amounts map[stri } transfer.ExternalID = fmt.Sprintf("%d", tx.ID) - transfer.ActualAmount = product.Destination.Amount return transfer, nil } diff --git a/services/airtime/dtone/service_test.go b/services/airtime/dtone/service_test.go index 7461dc74e..a64e8dfc2 100644 --- a/services/airtime/dtone/service_test.go +++ b/services/airtime/dtone/service_test.go @@ -50,20 +50,19 @@ func TestServiceWithSuccessfulTranfer(t *testing.T) { urns.URN("tel:+593979000000"), urns.URN("tel:+593979123456"), map[string]decimal.Decimal{ - "USD": decimal.RequireFromString("3.5"), + "USD": decimal.RequireFromString("3"), "RWF": decimal.RequireFromString("5000"), }, httpLogger.Log, ) assert.NoError(t, err) assert.Equal(t, &flows.AirtimeTransfer{ - UUID: "1ae96956-4b34-433e-8d1a-f05fe6923d6d", - ExternalID: "2237512891", - Sender: urns.URN("tel:+593979000000"), - Recipient: urns.URN("tel:+593979123456"), - Currency: "USD", - DesiredAmount: decimal.RequireFromString("3.5"), - ActualAmount: decimal.RequireFromString("3"), // closest product + UUID: "1ae96956-4b34-433e-8d1a-f05fe6923d6d", + ExternalID: "2237512891", + Sender: urns.URN("tel:+593979000000"), + Recipient: urns.URN("tel:+593979123456"), + Currency: "USD", + Amount: decimal.RequireFromString("3"), }, transfer) assert.Equal(t, 3, len(httpLogger.Logs)) @@ -85,12 +84,14 @@ func TestServiceFailedTransfers(t *testing.T) { httpx.NewMockResponse(200, nil, []byte(lookupNumberResponse)), httpx.NewMockResponse(200, nil, []byte(lookupNumberResponse)), httpx.NewMockResponse(200, nil, []byte(lookupNumberResponse)), + httpx.NewMockResponse(200, nil, []byte(lookupNumberResponse)), }, "https://dvs-api.dtone.com/v1/products?type=FIXED_VALUE_RECHARGE&operator_id=1596&per_page=100": { httpx.NewMockResponse(400, nil, errorResp(1003001, "Product is not available in your account")), httpx.NewMockResponse(200, nil, []byte(`[]`)), // no products httpx.NewMockResponse(200, nil, []byte(productsResponse)), httpx.NewMockResponse(200, nil, []byte(productsResponse)), + httpx.NewMockResponse(200, nil, []byte(productsResponse)), }, "https://dvs-api.dtone.com/v1/async/transactions": { httpx.NewMockResponse(400, nil, errorResp(1003001, "Something went wrong")), @@ -106,15 +107,14 @@ func TestServiceFailedTransfers(t *testing.T) { svc := dtone.NewService(http.DefaultClient, nil, "key123", "sesame") httpLogger := &flows.HTTPLogger{} - amounts := map[string]decimal.Decimal{"USD": decimal.RequireFromString("3.5")} + amounts := map[string]decimal.Decimal{"USD": decimal.RequireFromString("3")} // try when phone number lookup gives a connection error transfer, err := svc.Transfer(urns.URN("tel:+593979000000"), urns.URN("tel:+593979123456"), amounts, httpLogger.Log) assert.EqualError(t, err, "number lookup failed: unable to connect to server") assert.Equal(t, urns.URN("tel:+593979000000"), transfer.Sender) assert.Equal(t, urns.URN("tel:+593979123456"), transfer.Recipient) - assert.Equal(t, decimal.Zero, transfer.DesiredAmount) - assert.Equal(t, decimal.Zero, transfer.ActualAmount) + assert.Equal(t, decimal.Zero, transfer.Amount) // try when phone number lookup fails transfer, err = svc.Transfer(urns.URN("tel:+593979000000"), urns.URN("tel:+593979123456"), amounts, httpLogger.Log) @@ -136,6 +136,11 @@ func TestServiceFailedTransfers(t *testing.T) { assert.EqualError(t, err, "unable to find a suitable product for operator 'Claro Ecuador'") assert.NotNil(t, transfer) + // try when we can't find any suitable products (there are products but none match the amount) + transfer, err = svc.Transfer(urns.URN("tel:+593979000000"), urns.URN("tel:+593979123456"), map[string]decimal.Decimal{"USD": decimal.RequireFromString("2")}, httpLogger.Log) + assert.EqualError(t, err, "unable to find a suitable product for operator 'Claro Ecuador'") + assert.NotNil(t, transfer) + // try when transaction request errors transfer, err = svc.Transfer(urns.URN("tel:+593979000000"), urns.URN("tel:+593979123456"), amounts, httpLogger.Log) assert.EqualError(t, err, "transaction creation failed: Something went wrong") diff --git a/test/engine.go b/test/engine.go index 09960520a..ec96bdc06 100644 --- a/test/engine.go +++ b/test/engine.go @@ -121,12 +121,11 @@ func (s *airtimeService) Transfer(sender urns.URN, recipient urns.URN, amounts m } transfer := &flows.AirtimeTransfer{ - UUID: flows.AirtimeTransferUUID(uuids.NewV4()), - Sender: sender, - Recipient: recipient, - Currency: s.fixedCurrency, - DesiredAmount: amount, - ActualAmount: amount, + UUID: flows.AirtimeTransferUUID(uuids.NewV4()), + Sender: sender, + Recipient: recipient, + Currency: s.fixedCurrency, + Amount: amount, } return transfer, nil diff --git a/test/testdata/runner/airtime.json b/test/testdata/runner/airtime.json index 2a0d8955c..99a254f80 100644 --- a/test/testdata/runner/airtime.json +++ b/test/testdata/runner/airtime.json @@ -16,7 +16,7 @@ "uuid": "8720f157-ca1c-432f-9c0b-2014ddc77094", "amounts": { "RWF": 5000, - "USD": 3.5 + "USD": 3 }, "result_name": "Transfer" } diff --git a/test/testdata/runner/airtime.test_successful_transfer.json b/test/testdata/runner/airtime.test_successful_transfer.json index eb944249f..03b90a044 100644 --- a/test/testdata/runner/airtime.test_successful_transfer.json +++ b/test/testdata/runner/airtime.test_successful_transfer.json @@ -128,10 +128,9 @@ { "events": [ { - "actual_amount": 3, + "amount": 3, "created_on": "2018-07-06T12:30:08.123456789Z", "currency": "USD", - "desired_amount": 3.5, "external_id": "2237512891", "http_logs": [ { @@ -214,10 +213,9 @@ "created_on": "2018-07-06T12:30:00.123456789Z", "events": [ { - "actual_amount": 3, + "amount": 3, "created_on": "2018-07-06T12:30:08.123456789Z", "currency": "USD", - "desired_amount": 3.5, "external_id": "2237512891", "http_logs": [ {