Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: fawry connector scaffold #1643

Open
wants to merge 2 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all 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
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
package client

import (
"crypto/sha256"
"encoding/hex"
"strings"
)

type Client struct {
SecureKey string
MerchantCode string
}

func (c *Client) SignRequest(inputs []string) string {
inputs = append(inputs, c.SecureKey)
sum := sha256.Sum256([]byte(strings.Join(inputs[:], ",")))
return hex.EncodeToString(sum[:])
}

func (c *Client) Init() error {
return nil
}

func NewClient() *Client {
return &Client{}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
package client

type Notification struct {
RequestID string `json:"requestId"`

Amount string `json:"amount"`

PaymentID string `json:"paymentId"`
TransactionID string `json:"transactionId"`

BillingAccount string `json:"billingAcct"`
ExtraBillingAccounts any `json:"extraBillingAccts"`

CustomerReferenceNumber string `json:"customerReferenceNumber"`
TerminalID string `json:"terminalId"`
BillNumber string `json:"billNumber"`
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package fawry

import "encoding/json"

type Config struct {
Name string `json:"name" yaml:"name" bson:"name"`
Endpoint string `json:"endpoint" yaml:"endpoint" bson:"endpoint"`
SecureKey string `json:"secureKey" yaml:"secureKey" bson:"secureKey"`
MerchantCode string `json:"merchantCode" yaml:"merchantCode" bson:"merchantCode"`
}

func (c Config) Marshal() ([]byte, error) {
return json.Marshal(c)
}

func (c Config) ConnectorName() string {
return c.Name
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
package fawry

import (
"github.com/formancehq/payments/cmd/connectors/internal/task"
"github.com/formancehq/payments/internal/models"
"github.com/formancehq/stack/libs/go-libs/logging"
)

type Connector struct {
logger logging.Logger
cfg Config

taskMemoryState *TaskMemoryState
}

func (c *Connector) Install(ctx task.ConnectorContext) error {
desc, err := models.EncodeTaskDescriptor(TaskDescriptor{
Name: TaskMain,
})
if err != nil {
return err
}

err = ctx.Scheduler().Schedule(ctx.Context(), desc, models.TaskSchedulerOptions{
ScheduleOption: models.OPTIONS_RUN_NOW,
RestartOption: models.OPTIONS_RESTART_ALWAYS,
})
if err != nil {
return err
}

return nil
}

func (c *Connector) Uninstall(ctx task.ConnectorContext) error {
return nil
}
Comment on lines +35 to +37
Copy link
Contributor

Choose a reason for hiding this comment

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

Implement logic for Uninstall method.

The Uninstall method currently returns nil. If there are resources to clean up or tasks to cancel, implement the necessary logic here.

func (c *Connector) Uninstall(ctx task.ConnectorContext) error {
    // Implement uninstallation logic
    return nil
}


func (c *Connector) Resolve(descriptor models.TaskDescriptor) task.Task {
taskDescriptor, err := models.DecodeTaskDescriptor[TaskDescriptor](descriptor)
if err != nil {
panic(err)
}

return Resolve(c.logger, c.cfg, c.taskMemoryState)(taskDescriptor)
}

func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error {
panic("not supported")
}

func (c *Connector) InitiatePayment(ctx task.ConnectorContext, transfer *models.TransferInitiation) error {
panic("not supported")
}

// ReversePayment
func (c *Connector) ReversePayment(ctx task.ConnectorContext, reversal *models.TransferReversal) error {
panic("not supported")
}

// CreateExternalBankAccount
func (c *Connector) CreateExternalBankAccount(ctx task.ConnectorContext, account *models.BankAccount) error {
panic("not supported")
}
Comment on lines +48 to +64
Copy link
Contributor

Choose a reason for hiding this comment

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

Handle unsupported operations gracefully.

The methods UpdateConfig, InitiatePayment, ReversePayment, and CreateExternalBankAccount currently panic. Consider returning a descriptive error instead to avoid disrupting the application flow.

func (c *Connector) UpdateConfig(ctx task.ConnectorContext, config models.ConnectorConfigObject) error {
    return fmt.Errorf("operation not supported")
}


// SupportedCurrenciesAndDecimals
func (c *Connector) SupportedCurrenciesAndDecimals(ctx task.ConnectorContext) map[string]int {
return supportedCurrenciesWithDecimal
}

func newConnector() *Connector {
return &Connector{}
}
Comment on lines +71 to +73
Copy link
Contributor

Choose a reason for hiding this comment

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

Enhance newConnector function with initialization logic.

The newConnector function currently returns an empty Connector. If there are configurations or dependencies to initialize, consider adding them here.

func newConnector(logger logging.Logger, cfg Config) *Connector {
    return &Connector{
        logger: logger,
        cfg:    cfg,
        // Initialize other fields
    }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package fawry

import "github.com/formancehq/payments/cmd/connectors/internal/connectors/currency"

var (
supportedCurrenciesWithDecimal = map[string]int{
"EGP": currency.ISO4217Currencies["EGP"],
}
)
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
package fawry

func taskIngestPayment() error {
return nil
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package fawry

import (
"net/http"

"github.com/formancehq/payments/cmd/connectors/internal/storage"
"github.com/formancehq/payments/internal/models"
"github.com/formancehq/stack/libs/go-libs/logging"
"github.com/gorilla/mux"
)

type Loader struct{}

func (l *Loader) Name() models.ConnectorProvider {
return "fawry"
}

func (l *Loader) AllowTasks() int {
return 10
}

func (l *Loader) ApplyDefaults(cfg Config) Config {
return cfg
}

// Router returns the router for the connector, which we'll use to handle webhooks.
func (l *Loader) Router(store *storage.Storage) *mux.Router {
r := mux.NewRouter()

w := NewWebhook()

r.Path("/").Methods(http.MethodPost).HandlerFunc(w.Handle())

return r
}

func NewLoader() *Loader {
return &Loader{}
}

func (l *Loader) Load(logger logging.Logger, config Config) Connector {
return *newConnector()
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package fawry

import (
"fmt"
"sync"

"github.com/formancehq/payments/cmd/connectors/internal/connectors/fawry/client"
"github.com/formancehq/payments/cmd/connectors/internal/task"
"github.com/formancehq/stack/libs/go-libs/logging"
)

const (
TaskMain = "main"
TaskIngestPayment = "ingest-payment"
)

// internal state not pushed in the database
type TaskMemoryState struct {
// We want to fetch the transactions once per service start.
fetchTransactionsOnce map[string]*sync.Once
}

type TaskDescriptor struct {
Name string `json:"name" yaml:"name" bson:"name"`
Key string `json:"key" yaml:"key" bson:"key"`
Payload string `json:"payload" yaml:"payload" bson:"payload"`
}

func Resolve(
logger logging.Logger,
config Config,
taskMemoryState *TaskMemoryState,
) func(taskDefinition TaskDescriptor) task.Task {
client := client.NewClient()
err := client.Init()
if err != nil {
logger.Error(err)
return func(taskDescriptor TaskDescriptor) task.Task {
return func() error {
return fmt.Errorf("cannot build fawry client: %w", err)
}
}
}

return func(taskDescriptor TaskDescriptor) task.Task {
switch taskDescriptor.Key {
case TaskMain:
return func() error {
return nil
}
case TaskIngestPayment:
return taskIngestPayment
default:
return func() error {
return fmt.Errorf("unknown task key: %s", taskDescriptor.Key)
}
}
}
}
Comment on lines +29 to +59
Copy link
Contributor

Choose a reason for hiding this comment

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

Improve error handling in Resolve function.

The Resolve function logs an error and returns a task that will always fail if the client initialization fails. Consider implementing a retry mechanism or alerting system to handle such failures more gracefully.

if err != nil {
    logger.Error(err)
    // Implement retry logic or alerting
    return func(taskDescriptor TaskDescriptor) task.Task {
        return func() error {
            return fmt.Errorf("cannot build fawry client: %w", err)
        }
    }
}

Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package fawry

import (
"encoding/json"
"io"
"net/http"
"time"

"github.com/formancehq/payments/cmd/connectors/internal/connectors/fawry/client"
"github.com/formancehq/payments/cmd/connectors/internal/task"
"github.com/formancehq/payments/internal/models"
"github.com/formancehq/stack/libs/go-libs/contextutil"
)

type Webhook struct {
}

func (w *Webhook) Handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn := task.ConnectorContextFromContext(r.Context())

b, err := io.ReadAll(r.Body)

n := client.Notification{}
if err := json.Unmarshal(b, &n); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

ctx, _ := contextutil.DetachedWithTimeout(r.Context(), 30*time.Second)
td, err := models.EncodeTaskDescriptor(TaskDescriptor{
Name: TaskIngestPayment,
Key: TaskIngestPayment,
Payload: string(b),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}

conn.Scheduler().Schedule(ctx, td, models.TaskSchedulerOptions{
ScheduleOption: models.OPTIONS_RUN_NOW,
RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE,
})
}
Comment on lines +18 to +45
Copy link
Contributor

Choose a reason for hiding this comment

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

Enhance error handling in Handle method.

The method currently returns a 500 error for any JSON parsing issues. Consider using more specific status codes, such as 400 for bad requests, to better inform the client of the nature of the error.

-  http.Error(w, err.Error(), http.StatusInternalServerError)
+  http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func (w *Webhook) Handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn := task.ConnectorContextFromContext(r.Context())
b, err := io.ReadAll(r.Body)
n := client.Notification{}
if err := json.Unmarshal(b, &n); err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
ctx, _ := contextutil.DetachedWithTimeout(r.Context(), 30*time.Second)
td, err := models.EncodeTaskDescriptor(TaskDescriptor{
Name: TaskIngestPayment,
Key: TaskIngestPayment,
Payload: string(b),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
conn.Scheduler().Schedule(ctx, td, models.TaskSchedulerOptions{
ScheduleOption: models.OPTIONS_RUN_NOW,
RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE,
})
}
func (w *Webhook) Handle() http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
conn := task.ConnectorContextFromContext(r.Context())
b, err := io.ReadAll(r.Body)
n := client.Notification{}
if err := json.Unmarshal(b, &n); err != nil {
http.Error(w, "Invalid JSON payload", http.StatusBadRequest)
return
}
ctx, _ := contextutil.DetachedWithTimeout(r.Context(), 30*time.Second)
td, err := models.EncodeTaskDescriptor(TaskDescriptor{
Name: TaskIngestPayment,
Key: TaskIngestPayment,
Payload: string(b),
})
if err != nil {
http.Error(w, err.Error(), http.StatusInternalServerError)
return
}
conn.Scheduler().Schedule(ctx, td, models.TaskSchedulerOptions{
ScheduleOption: models.OPTIONS_RUN_NOW,
RestartOption: models.OPTIONS_RESTART_IF_NOT_ACTIVE,
})
}

}

func NewWebhook() *Webhook {
return &Webhook{}
}
Comment on lines +48 to +50
Copy link
Contributor

Choose a reason for hiding this comment

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

Consider dependency injection for NewWebhook.

The NewWebhook function currently returns an empty Webhook. If the Webhook requires dependencies, consider using dependency injection to pass them during initialization.

func NewWebhook(dependencies ...interface{}) *Webhook {
    return &Webhook{
        // Initialize with dependencies
    }
}

Loading