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

Refunds #4837

Merged
merged 11 commits into from
Jan 23, 2025
Prev Previous commit
Next Next commit
server: Remove refund.succeeded webhook
Better to listen to `order.refunded` for the intent and
`refund.succeeded` and `refund.created` would cause a race condition and
more nuanced implementations.

Good idea, but not entirely clear in practice.
birkjernstrom committed Jan 23, 2025
commit 9b5a5f694503c885a34d66e6810b45f81bf6c6b9
32 changes: 0 additions & 32 deletions clients/packages/api/src/client/models/index.ts
Original file line number Diff line number Diff line change
@@ -21601,7 +21601,6 @@ export const WebhookEventType = {
SUBSCRIPTION_UNCANCELED: 'subscription.uncanceled',
SUBSCRIPTION_REVOKED: 'subscription.revoked',
REFUND_CREATED: 'refund.created',
REFUND_SUCCEEDED: 'refund.succeeded',
REFUND_UPDATED: 'refund.updated',
PRODUCT_CREATED: 'product.created',
PRODUCT_UPDATED: 'product.updated',
@@ -21876,37 +21875,6 @@ export const WebhookRefundCreatedPayloadTypeEnum = {
} as const;
export type WebhookRefundCreatedPayloadTypeEnum = typeof WebhookRefundCreatedPayloadTypeEnum[keyof typeof WebhookRefundCreatedPayloadTypeEnum];

/**
* Sent when a refund succeeds.
*
* **Discord & Slack support:** Full
* @export
* @interface WebhookRefundSucceededPayload
*/
export interface WebhookRefundSucceededPayload {
/**
*
* @type {string}
* @memberof WebhookRefundSucceededPayload
*/
type: WebhookRefundSucceededPayloadTypeEnum;
/**
*
* @type {Refund}
* @memberof WebhookRefundSucceededPayload
*/
data: Refund;
}


/**
* @export
*/
export const WebhookRefundSucceededPayloadTypeEnum = {
REFUND_SUCCEEDED: 'refund.succeeded'
} as const;
export type WebhookRefundSucceededPayloadTypeEnum = typeof WebhookRefundSucceededPayloadTypeEnum[keyof typeof WebhookRefundSucceededPayloadTypeEnum];

/**
* Sent when a refund is updated.
*
1 change: 0 additions & 1 deletion server/polar/models/webhook_endpoint.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,6 @@ class WebhookEventType(StrEnum):
subscription_uncanceled = "subscription.uncanceled"
subscription_revoked = "subscription.revoked"
refund_created = "refund.created"
refund_succeeded = "refund.succeeded"
refund_updated = "refund.updated"
product_created = "product.created"
product_updated = "product.updated"
20 changes: 6 additions & 14 deletions server/polar/refund/service.py
Original file line number Diff line number Diff line change
@@ -306,7 +306,7 @@ async def update_from_stripe(

await self._on_updated(session, organization, refund)
if transitioned_to_succeeded:
await self._on_succeeded(session, organization, refund, order)
await self._on_succeeded(session, organization, order)
return refund

async def enqueue_benefits_revokation(
@@ -427,16 +427,15 @@ async def _create(
order: Order | None = None,
pledge: Pledge | None = None,
) -> Refund:
# Upsert to circumvent issues with race conditions from Stripe
# sending `refund.created` from our initial API call.
#
# We want to listen and create on that webhook in case of support
# admin in Stripe dashboard with manual refunds outside our system.
# Upsert to handle race condition from Stripe `refund.created`.
# Could be fired standalone from manual support operations in Stripe dashboard.
statement = (
postgresql.insert(Refund)
.values(**internal_create_schema.model_dump(by_alias=True))
.on_conflict_do_update(
index_elements=[Refund.processor_id],
# Only update `modified_at` as race conditions from API &
# webhook creation should only contain the same data.
set_=dict(
modified_at=utc_now(),
),
@@ -483,7 +482,7 @@ async def _create(

await self._on_created(session, organization, instance)
if instance.succeeded:
await self._on_succeeded(session, organization, instance, order)
await self._on_succeeded(session, organization, order)
return instance

async def _create_refund_transaction(
@@ -539,15 +538,8 @@ async def _on_succeeded(
self,
session: AsyncSession,
organization: Organization,
refund: Refund,
order: Order,
) -> None:
# Send refund.succeeded
await webhook_service.send(
session,
target=organization,
we=(WebhookEventType.refund_succeeded, refund),
)
# Send order.refunded
await webhook_service.send(
session,
53 changes: 0 additions & 53 deletions server/polar/webhook/webhooks.py
Original file line number Diff line number Diff line change
@@ -55,7 +55,6 @@
| tuple[Literal[WebhookEventType.subscription_canceled], Subscription]
| tuple[Literal[WebhookEventType.subscription_revoked], Subscription]
| tuple[Literal[WebhookEventType.refund_created], Refund]
| tuple[Literal[WebhookEventType.refund_succeeded], Refund]
| tuple[Literal[WebhookEventType.refund_updated], Refund]
| tuple[Literal[WebhookEventType.product_created], Product]
| tuple[Literal[WebhookEventType.product_updated], Product]
@@ -739,7 +738,6 @@ class WebhookRefundBase(BaseWebhookPayload):

type: (
Literal[WebhookEventType.refund_created]
| Literal[WebhookEventType.refund_succeeded]
| Literal[WebhookEventType.refund_updated]
)
data: RefundSchema
@@ -813,56 +811,6 @@ def get_slack_payload(self, target: User | Organization) -> str:
return json.dumps(payload)


class WebhookRefundSucceededPayload(WebhookRefundBase):
"""
Sent when a refund succeeds.

**Discord & Slack support:** Full
"""

type: Literal[WebhookEventType.refund_succeeded]
data: RefundSchema

def get_discord_payload(self, target: User | Organization) -> str:
if isinstance(target, User):
raise UnsupportedTarget(target, self.__class__, WebhookFormat.discord)

payload: DiscordPayload = {
"content": "Refund Succeeded",
"embeds": [
get_branded_discord_embed(
{
"title": "Refund Succeeded",
"description": f"Refund succeeded for {target.name}.",
"fields": self._get_discord_fields(target),
}
)
],
}
return json.dumps(payload)

def get_slack_payload(self, target: User | Organization) -> str:
if isinstance(target, User):
raise UnsupportedTarget(target, self.__class__, WebhookFormat.slack)

payload: SlackPayload = get_branded_slack_payload(
{
"text": "Refund Succeeded",
"blocks": [
{
"type": "section",
"text": {
"type": "mrkdwn",
"text": f"Refund succeeded for {target.name}.",
},
"fields": self._get_slack_fields(target),
}
],
}
)
return json.dumps(payload)


class WebhookRefundUpdatedPayload(WebhookRefundBase):
"""
Sent when a refund is updated.
@@ -1088,7 +1036,6 @@ class WebhookBenefitGrantRevokedPayload(BaseWebhookPayload):
| WebhookSubscriptionUncanceledPayload
| WebhookSubscriptionRevokedPayload
| WebhookRefundCreatedPayload
| WebhookRefundSucceededPayload
| WebhookRefundUpdatedPayload
| WebhookProductCreatedPayload
| WebhookProductUpdatedPayload