Skip to content

Commit 9f0a298

Browse files
authored
Merge pull request #277 from moovfinancial/add-webhooks
Adding in webhooks management to the api
2 parents 9f6b64e + d0eb863 commit 9f0a298

File tree

6 files changed

+274
-38
lines changed

6 files changed

+274
-38
lines changed

pkg/mhooks/event_type.go

Lines changed: 37 additions & 37 deletions
Original file line numberDiff line numberDiff line change
@@ -6,45 +6,45 @@ import (
66
"github.com/moovfinancial/moov-go/pkg/moov"
77
)
88

9-
type EventType string
9+
type EventType = moov.EventType
1010

1111
const (
12-
EventTypeAccountCreated EventType = "account.created"
13-
EventTypeAccountDisconnected EventType = "account.disconnected"
14-
EventTypeAccountUpdated EventType = "account.updated"
15-
EventTypeBalanceUpdated EventType = "balance.updated"
16-
EventTypeBankAccountCreated EventType = "bankAccount.created"
17-
EventTypeBankAccountDeleted EventType = "bankAccount.deleted"
18-
EventTypeBankAccountUpdated EventType = "bankAccount.updated"
19-
EventTypeBillingStatementCreated EventType = "billingStatement.created"
20-
EventTypeCancellationCreated EventType = "cancellation.created"
21-
EventTypeCancellationUpdated EventType = "cancellation.updated"
22-
EventTypeCardAutoUpdated EventType = "card.autoUpdated"
23-
EventTypeCapabilityRequested EventType = "capability.requested"
24-
EventTypeCapabilityUpdated EventType = "capability.updated"
25-
EventTypeDisputeCreated EventType = "dispute.created"
26-
EventTypeDisputeUpdated EventType = "dispute.updated"
27-
EventTypeInvoiceCreated EventType = "invoice.created"
28-
EventTypeInvoiceUpdated EventType = "invoice.updated"
29-
EventTypeNetworkIDUpdated EventType = "networkID.updated"
30-
EventTypePaymentMethodDisabled EventType = "paymentMethod.disabled"
31-
EventTypePaymentMethodEnabled EventType = "paymentMethod.enabled"
32-
EventTypeRefundCreated EventType = "refund.created"
33-
EventTypeRefundUpdated EventType = "refund.updated"
34-
EventTypeRepresentativeCreated EventType = "representative.created"
35-
EventTypeRepresentativeDeleted EventType = "representative.deleted"
36-
EventTypeRepresentativeUpdated EventType = "representative.updated"
37-
EventTypeSweepCreated EventType = "sweep.created"
38-
EventTypeSweepUpdated EventType = "sweep.updated"
39-
EventTypeTestPing EventType = "event.test"
40-
EventTypeTicketCreated EventType = "ticket.created"
41-
EventTypeTicketUpdated EventType = "ticket.updated"
42-
EventTypeTicketMessageAdded EventType = "ticket.messageAdded"
43-
EventTypeTransferCreated EventType = "transfer.created"
44-
EventTypeTransferUpdated EventType = "transfer.updated"
45-
EventTypeWalletCreated EventType = "wallet.created"
46-
EventTypeWalletUpdated EventType = "wallet.updated"
47-
EventTypeWalletTransactionUpdated EventType = "walletTransaction.updated"
12+
EventTypeAccountCreated = moov.EventTypeAccountCreated
13+
EventTypeAccountDisconnected = moov.EventTypeAccountDisconnected
14+
EventTypeAccountUpdated = moov.EventTypeAccountUpdated
15+
EventTypeBalanceUpdated = moov.EventTypeBalanceUpdated
16+
EventTypeBankAccountCreated = moov.EventTypeBankAccountCreated
17+
EventTypeBankAccountDeleted = moov.EventTypeBankAccountDeleted
18+
EventTypeBankAccountUpdated = moov.EventTypeBankAccountUpdated
19+
EventTypeBillingStatementCreated = moov.EventTypeBillingStatementCreated
20+
EventTypeCancellationCreated = moov.EventTypeCancellationCreated
21+
EventTypeCancellationUpdated = moov.EventTypeCancellationUpdated
22+
EventTypeCardAutoUpdated = moov.EventTypeCardAutoUpdated
23+
EventTypeCapabilityRequested = moov.EventTypeCapabilityRequested
24+
EventTypeCapabilityUpdated = moov.EventTypeCapabilityUpdated
25+
EventTypeDisputeCreated = moov.EventTypeDisputeCreated
26+
EventTypeDisputeUpdated = moov.EventTypeDisputeUpdated
27+
EventTypeInvoiceCreated = moov.EventTypeInvoiceCreated
28+
EventTypeInvoiceUpdated = moov.EventTypeInvoiceUpdated
29+
EventTypeNetworkIDUpdated = moov.EventTypeNetworkIDUpdated
30+
EventTypePaymentMethodDisabled = moov.EventTypePaymentMethodDisabled
31+
EventTypePaymentMethodEnabled = moov.EventTypePaymentMethodEnabled
32+
EventTypeRefundCreated = moov.EventTypeRefundCreated
33+
EventTypeRefundUpdated = moov.EventTypeRefundUpdated
34+
EventTypeRepresentativeCreated = moov.EventTypeRepresentativeCreated
35+
EventTypeRepresentativeDeleted = moov.EventTypeRepresentativeDeleted
36+
EventTypeRepresentativeUpdated = moov.EventTypeRepresentativeUpdated
37+
EventTypeSweepCreated = moov.EventTypeSweepCreated
38+
EventTypeSweepUpdated = moov.EventTypeSweepUpdated
39+
EventTypeTestPing = moov.EventTypeTestPing
40+
EventTypeTicketCreated = moov.EventTypeTicketCreated
41+
EventTypeTicketUpdated = moov.EventTypeTicketUpdated
42+
EventTypeTicketMessageAdded = moov.EventTypeTicketMessageAdded
43+
EventTypeTransferCreated = moov.EventTypeTransferCreated
44+
EventTypeTransferUpdated = moov.EventTypeTransferUpdated
45+
EventTypeWalletCreated = moov.EventTypeWalletCreated
46+
EventTypeWalletUpdated = moov.EventTypeWalletUpdated
47+
EventTypeWalletTransactionUpdated = moov.EventTypeWalletTransactionUpdated
4848
)
4949

5050
type AccountCreated struct {

pkg/moov/connections_test.go

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ package moov_test
22

33
import (
44
"testing"
5+
"time"
56

67
"github.com/moovfinancial/moov-go/pkg/moov"
78
"github.com/moovfinancial/moov-go/pkg/mv2507"
@@ -28,7 +29,14 @@ func Test_ShareConnection(t *testing.T) {
2829
})
2930

3031
t.Run("list accounts connected to merchant from the partner perspective", func(t *testing.T) {
31-
connections, err := mv2507.Accounts.ListConnected(BgCtx(), *mc, merchant.AccountID)
32+
var connections []mv2507.Account
33+
var err error
34+
35+
require.Eventually(t, func() bool {
36+
connections, err = mv2507.Accounts.ListConnected(BgCtx(), *mc, merchant.AccountID)
37+
return err == nil && len(connections) > 0
38+
}, time.Second*1, time.Millisecond*100)
39+
3240
require.NoError(t, err)
3341
require.NotNil(t, connections)
3442
require.Len(t, connections, 1)

pkg/moov/paths.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ const (
4747
pathWalletTransactions = "/accounts/%s/wallets/%s/transactions"
4848
pathWalletTransaction = "/accounts/%s/wallets/%s/transactions/%s"
4949

50+
pathWebhooks = "/webhooks"
51+
pathWebhook = "/webhooks/%s"
52+
5053
pathSweepConfigs = "/accounts/%s/sweep-configs"
5154
pathSweepConfig = "/accounts/%s/sweep-configs/%s"
5255

pkg/moov/webhook.go

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
package moov
2+
3+
import (
4+
"context"
5+
"net/http"
6+
)
7+
8+
func (c *Client) CreateWebhook(ctx context.Context, webhook UpsertWebhook) (*Webhook, error) {
9+
resp, err := c.CallHttp(ctx,
10+
Endpoint(http.MethodPost, pathWebhooks),
11+
AcceptJson(),
12+
JsonBody(webhook))
13+
if err != nil {
14+
return nil, err
15+
}
16+
17+
return CompletedObjectOrError[Webhook](resp)
18+
}
19+
20+
func (c *Client) ListWebhooks(ctx context.Context) ([]Webhook, error) {
21+
resp, err := c.CallHttp(ctx,
22+
Endpoint(http.MethodGet, pathWebhooks),
23+
AcceptJson())
24+
if err != nil {
25+
return nil, err
26+
}
27+
28+
return CompletedListOrError[Webhook](resp)
29+
}
30+
31+
func (c *Client) GetWebhook(ctx context.Context, webhookID string) (*Webhook, error) {
32+
resp, err := c.CallHttp(ctx,
33+
Endpoint(http.MethodGet, pathWebhook, webhookID),
34+
AcceptJson())
35+
if err != nil {
36+
return nil, err
37+
}
38+
39+
return CompletedObjectOrError[Webhook](resp)
40+
}
41+
42+
func (c *Client) UpdateWebhook(ctx context.Context, webhookID string, webhook UpsertWebhook) (*Webhook, error) {
43+
resp, err := c.CallHttp(ctx,
44+
Endpoint(http.MethodPut, pathWebhook, webhookID),
45+
AcceptJson(),
46+
JsonBody(webhook))
47+
if err != nil {
48+
return nil, err
49+
}
50+
51+
return CompletedObjectOrError[Webhook](resp)
52+
}
53+
54+
func (c *Client) DeleteWebhook(ctx context.Context, webhookID string) error {
55+
resp, err := c.CallHttp(ctx,
56+
Endpoint(http.MethodDelete, pathWebhook, webhookID),
57+
AcceptJson())
58+
if err != nil {
59+
return err
60+
}
61+
62+
return CompletedNilOrError(resp)
63+
}

pkg/moov/webhook_models.go

Lines changed: 71 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,71 @@
1+
package moov
2+
3+
import (
4+
"time"
5+
)
6+
7+
type EventType string
8+
9+
const (
10+
EventTypeAccountCreated EventType = "account.created"
11+
EventTypeAccountDisconnected EventType = "account.disconnected"
12+
EventTypeAccountUpdated EventType = "account.updated"
13+
EventTypeBalanceUpdated EventType = "balance.updated"
14+
EventTypeBankAccountCreated EventType = "bankAccount.created"
15+
EventTypeBankAccountDeleted EventType = "bankAccount.deleted"
16+
EventTypeBankAccountUpdated EventType = "bankAccount.updated"
17+
EventTypeBillingStatementCreated EventType = "billingStatement.created"
18+
EventTypeCancellationCreated EventType = "cancellation.created"
19+
EventTypeCancellationUpdated EventType = "cancellation.updated"
20+
EventTypeCardAutoUpdated EventType = "card.autoUpdated"
21+
EventTypeCapabilityRequested EventType = "capability.requested"
22+
EventTypeCapabilityUpdated EventType = "capability.updated"
23+
EventTypeDisputeCreated EventType = "dispute.created"
24+
EventTypeDisputeUpdated EventType = "dispute.updated"
25+
EventTypeInvoiceCreated EventType = "invoice.created"
26+
EventTypeInvoiceUpdated EventType = "invoice.updated"
27+
EventTypeNetworkIDUpdated EventType = "networkID.updated"
28+
EventTypePaymentMethodDisabled EventType = "paymentMethod.disabled"
29+
EventTypePaymentMethodEnabled EventType = "paymentMethod.enabled"
30+
EventTypeRefundCreated EventType = "refund.created"
31+
EventTypeRefundUpdated EventType = "refund.updated"
32+
EventTypeRepresentativeCreated EventType = "representative.created"
33+
EventTypeRepresentativeDeleted EventType = "representative.deleted"
34+
EventTypeRepresentativeUpdated EventType = "representative.updated"
35+
EventTypeSweepCreated EventType = "sweep.created"
36+
EventTypeSweepUpdated EventType = "sweep.updated"
37+
EventTypeTestPing EventType = "event.test"
38+
EventTypeTicketCreated EventType = "ticket.created"
39+
EventTypeTicketUpdated EventType = "ticket.updated"
40+
EventTypeTicketMessageAdded EventType = "ticket.messageAdded"
41+
EventTypeTransferCreated EventType = "transfer.created"
42+
EventTypeTransferUpdated EventType = "transfer.updated"
43+
EventTypeWalletCreated EventType = "wallet.created"
44+
EventTypeWalletUpdated EventType = "wallet.updated"
45+
EventTypeWalletTransactionUpdated EventType = "walletTransaction.updated"
46+
)
47+
48+
const (
49+
WebhookStatusEnabled WebhookStatus = "enabled"
50+
WebhookStatusDisabled WebhookStatus = "disabled"
51+
)
52+
53+
type WebhookStatus string
54+
55+
type UpsertWebhook struct {
56+
URL string `json:"url"`
57+
Description string `json:"description"`
58+
Status WebhookStatus `json:"status"`
59+
EventTypes []EventType `json:"eventTypes"`
60+
}
61+
62+
type Webhook struct {
63+
WebhookID string `json:"webhookID"`
64+
URL string `json:"url"`
65+
Description string `json:"description,omitempty"`
66+
Status WebhookStatus `json:"status"`
67+
LastUsedOn *time.Time `json:"lastUsedOn,omitempty"`
68+
EventTypes []EventType `json:"eventTypes"`
69+
CreatedOn time.Time `json:"createdOn"`
70+
UpdatedOn time.Time `json:"updatedOn"`
71+
}

pkg/moov/webhook_test.go

Lines changed: 91 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,91 @@
1+
package moov_test
2+
3+
import (
4+
"context"
5+
"slices"
6+
"testing"
7+
"time"
8+
9+
"github.com/moovfinancial/moov-go/pkg/moov"
10+
"github.com/stretchr/testify/require"
11+
)
12+
13+
func TestCreateWebhook(t *testing.T) {
14+
mc := NewTestClient(t)
15+
16+
createWebhook := moov.UpsertWebhook{
17+
URL: "https://example.com/webhook",
18+
Description: "Test webhook",
19+
Status: moov.WebhookStatusEnabled,
20+
EventTypes: []moov.EventType{
21+
moov.EventTypeAccountCreated,
22+
moov.EventTypeAccountDisconnected,
23+
moov.EventTypeAccountUpdated,
24+
},
25+
}
26+
27+
createdWebhook, err := mc.CreateWebhook(context.Background(), createWebhook)
28+
require.NoError(t, err)
29+
require.NotNil(t, createdWebhook)
30+
31+
normalizeWebhook(createdWebhook)
32+
33+
t.Cleanup(func() {
34+
if createdWebhook != nil {
35+
mc.DeleteWebhook(context.Background(), createdWebhook.WebhookID)
36+
createdWebhook = nil
37+
}
38+
})
39+
40+
t.Run("list webhooks", func(t *testing.T) {
41+
webhooks, err := mc.ListWebhooks(context.Background())
42+
for i := range webhooks {
43+
normalizeWebhook(&webhooks[i])
44+
}
45+
46+
require.NoError(t, err)
47+
require.NotNil(t, webhooks)
48+
require.Contains(t, webhooks, *createdWebhook)
49+
})
50+
51+
t.Run("get webhook", func(t *testing.T) {
52+
webhook, err := mc.GetWebhook(context.Background(), createdWebhook.WebhookID)
53+
normalizeWebhook(webhook)
54+
require.NoError(t, err)
55+
require.NotNil(t, webhook)
56+
require.Equal(t, createdWebhook, webhook)
57+
})
58+
59+
t.Run("update webhook", func(t *testing.T) {
60+
webhook, err := mc.UpdateWebhook(context.Background(), createdWebhook.WebhookID, moov.UpsertWebhook{
61+
URL: "https://example.com/webhook-new",
62+
Description: "Test webhook new",
63+
Status: moov.WebhookStatusEnabled,
64+
EventTypes: []moov.EventType{
65+
moov.EventTypeAccountCreated,
66+
moov.EventTypeAccountDisconnected,
67+
moov.EventTypeAccountUpdated,
68+
},
69+
})
70+
normalizeWebhook(webhook)
71+
require.NoError(t, err)
72+
require.NotNil(t, webhook)
73+
require.Equal(t, "https://example.com/webhook-new", webhook.URL)
74+
require.Equal(t, "Test webhook new", webhook.Description)
75+
})
76+
77+
t.Run("delete webhook", func(t *testing.T) {
78+
err := mc.DeleteWebhook(context.Background(), createdWebhook.WebhookID)
79+
require.NoError(t, err)
80+
})
81+
}
82+
83+
func normalizeWebhook(webhook *moov.Webhook) {
84+
if webhook == nil {
85+
return
86+
}
87+
88+
webhook.CreatedOn = webhook.CreatedOn.Truncate(time.Microsecond)
89+
webhook.UpdatedOn = webhook.UpdatedOn.Truncate(time.Microsecond)
90+
slices.Sort(webhook.EventTypes)
91+
}

0 commit comments

Comments
 (0)