Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions controllers/sender/sender.go
Original file line number Diff line number Diff line change
Expand Up @@ -1344,6 +1344,7 @@ func (ctrl *SenderController) GetPaymentOrderByID(ctx *gin.Context) {
UpdatedAt: paymentOrder.UpdatedAt,
TxHash: paymentOrder.TxHash,
Status: paymentOrder.Status,
RefundReason: refundReasonFromOrder(paymentOrder.Status, paymentOrder.CancellationReasons),
OrderType: paymentOrder.OrderType,
})
}
Expand Down Expand Up @@ -1706,6 +1707,14 @@ func (ctrl *SenderController) applyFilters(ctx *gin.Context, query *ent.PaymentO
return query
}

// refundReasonFromOrder returns a joined refund reason string when status is refunded and reasons exist
func refundReasonFromOrder(status paymentorder.Status, reasons []string) string {
if status != paymentorder.StatusRefunded || reasons == nil || len(reasons) == 0 {
return ""
}
return strings.Join(reasons, "; ")
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@onahprosper this could contain duplicate reasons

}

// buildPaymentOrderResponses converts ent PaymentOrder entities to API response format
func (ctrl *SenderController) buildPaymentOrderResponses(ctx *gin.Context, paymentOrders []*ent.PaymentOrder) ([]types.SenderOrderResponse, error) {
// Batch fetch institutions to avoid N+1 queries
Expand Down Expand Up @@ -1778,6 +1787,7 @@ func (ctrl *SenderController) buildPaymentOrderResponses(ctx *gin.Context, payme
UpdatedAt: paymentOrder.UpdatedAt,
TxHash: paymentOrder.TxHash,
Status: paymentOrder.Status,
RefundReason: refundReasonFromOrder(paymentOrder.Status, paymentOrder.CancellationReasons),
OrderType: paymentOrder.OrderType,
})
}
Expand Down
46 changes: 25 additions & 21 deletions controllers/sender/sender_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -177,7 +177,6 @@ func setup() error {
"fee_percent": "5",
"token": token.Symbol,
})

if err != nil {
return fmt.Errorf("CreateTestSenderProfile.sender_test: %w", err)
}
Expand Down Expand Up @@ -274,7 +273,6 @@ func setupHTTPMocks() {
}

func TestSender(t *testing.T) {

// Set up test database client with shared in-memory schema so all connections see the same tables
client := enttest.Open(t, "sqlite3", "file:ent?mode=memory&cache=shared&_fk=1")
defer client.Close()
Expand Down Expand Up @@ -472,7 +470,6 @@ func TestSender(t *testing.T) {
})

t.Run("should successfully create order with valid amount", func(t *testing.T) {

// Activate httpmock globally to intercept all HTTP calls (including fastshot)
httpmock.Activate()
defer httpmock.DeactivateAndReset()
Expand Down Expand Up @@ -1141,7 +1138,6 @@ func TestSender(t *testing.T) {
assert.NoError(t, err)
assert.Equal(t, expectedFee, paymentOrder.SenderFee, "Database should store calculated fee")
})

})

t.Run("GetPaymentOrderByID", func(t *testing.T) {
Expand Down Expand Up @@ -1198,12 +1194,20 @@ func TestSender(t *testing.T) {
}
assert.NotEmpty(t, data["total"])
assert.NotEmpty(t, data["orders"])
if orders, ok := data["orders"].([]interface{}); ok {
for _, o := range orders {
if order, ok := o.(map[string]interface{}); ok && order["refundReason"] != nil {
_, ok := order["refundReason"].(string)
assert.True(t, ok, "refundReason when present must be a string")
}
}
}
}
})

t.Run("when filtering is applied", func(t *testing.T) {
// Test different status filters
var payload = map[string]interface{}{
payload := map[string]interface{}{
"status": "initiated",
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1238,7 +1242,7 @@ func TestSender(t *testing.T) {
// Test different page and pageSize values
page := 1
pageSize := 10
var payload = map[string]interface{}{
payload := map[string]interface{}{
"page": strconv.Itoa(page),
"pageSize": strconv.Itoa(pageSize),
"timestamp": time.Now().Unix(),
Expand Down Expand Up @@ -1273,7 +1277,7 @@ func TestSender(t *testing.T) {

t.Run("with ordering", func(t *testing.T) {
// Test ascending and descending ordering
var payload = map[string]interface{}{
payload := map[string]interface{}{
"ordering": "desc",
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1318,7 +1322,7 @@ func TestSender(t *testing.T) {
})

t.Run("with filtering by network", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"network": testCtx.networkIdentifier,
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1353,7 +1357,7 @@ func TestSender(t *testing.T) {
})

t.Run("with filtering by token", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"token": testCtx.token.Symbol,
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1417,7 +1421,7 @@ func TestSender(t *testing.T) {
return
}

var payload = map[string]interface{}{
payload := map[string]interface{}{
"timestamp": time.Now().Unix(),
}

Expand Down Expand Up @@ -1457,7 +1461,7 @@ func TestSender(t *testing.T) {
})

t.Run("when orders have been initiated", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"timestamp": time.Now().Unix(),
}

Expand Down Expand Up @@ -1536,7 +1540,7 @@ func TestSender(t *testing.T) {
Save(context.Background())
assert.NoError(t, err)
assert.NoError(t, err)
var payload = map[string]interface{}{
payload := map[string]interface{}{
"timestamp": time.Now().Unix(),
}

Expand Down Expand Up @@ -1588,7 +1592,7 @@ func TestSender(t *testing.T) {

t.Run("SearchPaymentOrders", func(t *testing.T) {
t.Run("should return error when search query is empty", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"search": "",
"timestamp": time.Now().Unix(),
}
Expand All @@ -1610,7 +1614,7 @@ func TestSender(t *testing.T) {
})

t.Run("should search by account identifier", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"search": "1234567890",
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1641,7 +1645,7 @@ func TestSender(t *testing.T) {
})

t.Run("should return empty results for non-matching search", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"search": "nonexistent_search_term",
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1727,7 +1731,7 @@ func TestSender(t *testing.T) {
Save(context.Background())
assert.NoError(t, err)

var payload = map[string]interface{}{
payload := map[string]interface{}{
"search": paymentOrder2.ID.String(),
"timestamp": time.Now().Unix(),
}
Expand Down Expand Up @@ -1784,7 +1788,7 @@ func TestSender(t *testing.T) {
today := time.Now().Format("2006-01-02")
tomorrow := time.Now().Add(24 * time.Hour).Format("2006-01-02")

var payload = map[string]interface{}{
payload := map[string]interface{}{
"from": today,
"to": tomorrow,
"export": "csv",
Expand Down Expand Up @@ -1823,7 +1827,7 @@ func TestSender(t *testing.T) {
today := time.Now().Format("2006-01-02")
tomorrow := time.Now().Add(24 * time.Hour).Format("2006-01-02")

var payload = map[string]interface{}{
payload := map[string]interface{}{
"from": today,
"to": tomorrow,
"limit": "50", // Use a limit higher than expected orders to avoid validation error
Expand Down Expand Up @@ -1858,7 +1862,7 @@ func TestSender(t *testing.T) {
today := time.Now().Format("2006-01-02")
tomorrow := time.Now().Add(24 * time.Hour).Format("2006-01-02")

var payload = map[string]interface{}{
payload := map[string]interface{}{
"from": today,
"to": tomorrow,
"limit": "1", // Very small limit to trigger validation
Expand All @@ -1883,7 +1887,7 @@ func TestSender(t *testing.T) {
})

t.Run("should return error for invalid date format", func(t *testing.T) {
var payload = map[string]interface{}{
payload := map[string]interface{}{
"from": "invalid-date",
"to": "2024-12-31",
"export": "csv",
Expand Down Expand Up @@ -1965,7 +1969,7 @@ func TestSender(t *testing.T) {
today := time.Now().Format("2006-01-02")
tomorrow := time.Now().Add(24 * time.Hour).Format("2006-01-02")

var payload = map[string]interface{}{
payload := map[string]interface{}{
"from": today,
"to": tomorrow,
"export": "csv",
Expand Down
17 changes: 17 additions & 0 deletions tasks/stale_ops.go
Original file line number Diff line number Diff line change
Expand Up @@ -329,6 +329,23 @@ func RetryStaleUserOperations() error {
} else {
service = orderService.NewOrderEVM()
}
if len(order.CancellationReasons) == 0 {
_, err := storage.Client.PaymentOrder.
Update().
Where(
paymentorder.IDEQ(order.ID),
paymentorder.CancellationReasonsIsNil(),
).
SetCancellationReasons([]string{"Order timed out"}).
Save(ctx)
if err != nil {
// The refund should still happen, so we log the error and continue
logger.WithFields(logger.Fields{
"Error": fmt.Sprintf("%v", err),
"OrderID": order.ID.String(),
}).Errorf("RetryStaleUserOperations.RefundOrder.SetCancellationReasons")
}
}
err := service.RefundOrder(ctx, order.Edges.Token.Edges.Network, order.GatewayID)
if err != nil {
logger.WithFields(logger.Fields{
Expand Down
2 changes: 2 additions & 0 deletions types/types.go
Original file line number Diff line number Diff line change
Expand Up @@ -496,6 +496,7 @@ type SenderOrderResponse struct {
UpdatedAt time.Time `json:"updatedAt"`
TxHash string `json:"txHash"`
Status paymentorder.Status `json:"status"`
RefundReason string `json:"refundReason,omitempty"`
Transactions []TransactionLog `json:"transactionLogs"`
OrderType paymentorder.OrderType `json:"orderType"`
}
Expand All @@ -522,6 +523,7 @@ type PaymentOrderWebhookData struct {
CreatedAt time.Time `json:"createdAt"`
TxHash string `json:"txHash"`
Status paymentorder.Status `json:"status"`
RefundReason string `json:"refundReason,omitempty"`
}

// PaymentOrderWebhookPayload is the request type for a payment order webhook
Expand Down
10 changes: 9 additions & 1 deletion utils/utils.go
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,6 @@ func StringToByte32(s string) [32]byte {

// Byte32ToString converts [32]byte to string
func Byte32ToString(b [32]byte) string {

// Find first null index if any
nullIndex := -1
for i, x := range b {
Expand Down Expand Up @@ -300,6 +299,14 @@ func SendPaymentOrderWebhook(ctx context.Context, paymentOrder *ent.PaymentOrder
if paymentOrder.Edges.Provider != nil {
providerID = paymentOrder.Edges.Provider.ID
}
reasons := paymentOrder.CancellationReasons
if reasons == nil {
reasons = []string{}
}
refundReason := ""
if (paymentOrder.Status == paymentorder.StatusRefunded || paymentOrder.Status == paymentorder.StatusRefunding) && len(reasons) > 0 {
refundReason = strings.Join(reasons, "; ")
}
Comment on lines +307 to +309
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@onahprosper this could contain duplicate reasons

payloadStruct := types.PaymentOrderWebhookPayload{
Event: event,
Data: types.PaymentOrderWebhookData{
Expand Down Expand Up @@ -329,6 +336,7 @@ func SendPaymentOrderWebhook(ctx context.Context, paymentOrder *ent.PaymentOrder
CreatedAt: paymentOrder.CreatedAt,
TxHash: paymentOrder.TxHash,
Status: paymentOrder.Status,
RefundReason: refundReason,
},
}

Expand Down
Loading