-
-
Notifications
You must be signed in to change notification settings - Fork 49
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
base: main
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
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 | ||
} | ||
|
||
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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Handle unsupported operations gracefully. The methods 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enhance The 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Improve error handling in The 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
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Enhance error handling in 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
Suggested change
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
|
||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
func NewWebhook() *Webhook { | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
return &Webhook{} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
} | ||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||||
Comment on lines
+48
to
+50
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider dependency injection for The func NewWebhook(dependencies ...interface{}) *Webhook {
return &Webhook{
// Initialize with dependencies
}
} |
There was a problem hiding this comment.
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.