feat: create customer id from email if missing
This commit is contained in:
@@ -95,6 +95,14 @@ class FiatSubscriptionPaymentOptions(BaseModel):
|
|||||||
description="Unique ID that can be used to identify the subscription request."
|
description="Unique ID that can be used to identify the subscription request."
|
||||||
"If not provided, one will be generated.",
|
"If not provided, one will be generated.",
|
||||||
)
|
)
|
||||||
|
customer_id: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="The fiat provider customer ID to use for the subscription.",
|
||||||
|
)
|
||||||
|
customer_email: str | None = Field(
|
||||||
|
default=None,
|
||||||
|
description="The customer email to use for the subscription.",
|
||||||
|
)
|
||||||
tag: str | None = Field(
|
tag: str | None = Field(
|
||||||
default=None,
|
default=None,
|
||||||
description="Payments created by the recurring subscription"
|
description="Payments created by the recurring subscription"
|
||||||
|
|||||||
+32
-9
@@ -1,8 +1,8 @@
|
|||||||
import asyncio
|
import asyncio
|
||||||
import ipaddress
|
import ipaddress
|
||||||
import json
|
import json
|
||||||
from decimal import Decimal, ROUND_HALF_UP
|
|
||||||
from collections.abc import AsyncGenerator
|
from collections.abc import AsyncGenerator
|
||||||
|
from decimal import ROUND_HALF_UP, Decimal
|
||||||
from typing import Any
|
from typing import Any
|
||||||
from urllib.parse import urlparse
|
from urllib.parse import urlparse
|
||||||
|
|
||||||
@@ -212,13 +212,6 @@ class RevolutWallet(FiatProvider):
|
|||||||
)
|
)
|
||||||
|
|
||||||
extra = payment_options.extra or {}
|
extra = payment_options.extra or {}
|
||||||
customer_id = extra.get("customer_id")
|
|
||||||
if not customer_id:
|
|
||||||
return FiatSubscriptionResponse(
|
|
||||||
ok=False,
|
|
||||||
error_message="Revolut subscriptions require extra.customer_id.",
|
|
||||||
)
|
|
||||||
|
|
||||||
if not payment_options.subscription_request_id:
|
if not payment_options.subscription_request_id:
|
||||||
payment_options.subscription_request_id = urlsafe_short_hash()
|
payment_options.subscription_request_id = urlsafe_short_hash()
|
||||||
|
|
||||||
@@ -231,7 +224,6 @@ class RevolutWallet(FiatProvider):
|
|||||||
)
|
)
|
||||||
payload: dict[str, Any] = {
|
payload: dict[str, Any] = {
|
||||||
"plan_variation_id": subscription_id,
|
"plan_variation_id": subscription_id,
|
||||||
"customer_id": customer_id,
|
|
||||||
"external_reference": self._serialize_subscription_reference(reference),
|
"external_reference": self._serialize_subscription_reference(reference),
|
||||||
"setup_order_redirect_url": (
|
"setup_order_redirect_url": (
|
||||||
payment_options.success_url
|
payment_options.success_url
|
||||||
@@ -248,6 +240,12 @@ class RevolutWallet(FiatProvider):
|
|||||||
}
|
}
|
||||||
|
|
||||||
try:
|
try:
|
||||||
|
customer_id, customer_error = await self._get_subscription_customer_id(
|
||||||
|
payment_options
|
||||||
|
)
|
||||||
|
if not customer_id:
|
||||||
|
return FiatSubscriptionResponse(ok=False, error_message=customer_error)
|
||||||
|
payload["customer_id"] = customer_id
|
||||||
r = await self.client.post(
|
r = await self.client.post(
|
||||||
"/api/subscriptions", json=payload, headers=headers
|
"/api/subscriptions", json=payload, headers=headers
|
||||||
)
|
)
|
||||||
@@ -351,6 +349,31 @@ class RevolutWallet(FiatProvider):
|
|||||||
r.raise_for_status()
|
r.raise_for_status()
|
||||||
return r.json()
|
return r.json()
|
||||||
|
|
||||||
|
async def _get_subscription_customer_id(
|
||||||
|
self, payment_options: FiatSubscriptionPaymentOptions
|
||||||
|
) -> tuple[str | None, str | None]:
|
||||||
|
if payment_options.customer_id:
|
||||||
|
return payment_options.customer_id, None
|
||||||
|
if not payment_options.customer_email:
|
||||||
|
payment_options.customer_email = "test@lnbits.com"
|
||||||
|
# TODO: Remove the above line and uncomment the
|
||||||
|
# below return statement once we require customer_email for subscriptions.
|
||||||
|
# return (
|
||||||
|
# None,
|
||||||
|
# "Revolut subscriptions require customer_id or customer_email.",
|
||||||
|
# )
|
||||||
|
|
||||||
|
customer = await self.create_customer(payment_options.customer_email)
|
||||||
|
customer_id = customer.get("id")
|
||||||
|
if not customer_id:
|
||||||
|
return None, "Server error: missing customer id"
|
||||||
|
return customer_id, None
|
||||||
|
|
||||||
|
async def create_customer(self, email: str) -> dict[str, Any]:
|
||||||
|
r = await self.client.post("/api/customers", json={"email": email})
|
||||||
|
r.raise_for_status()
|
||||||
|
return r.json()
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
async def create_webhook(
|
async def create_webhook(
|
||||||
cls,
|
cls,
|
||||||
|
|||||||
@@ -896,7 +896,8 @@ async def test_revolut_wallet_create_subscription(settings: Settings):
|
|||||||
wallet_id="wallet_1",
|
wallet_id="wallet_1",
|
||||||
memo="Monthly Gold",
|
memo="Monthly Gold",
|
||||||
tag="gold",
|
tag="gold",
|
||||||
extra={"customer_id": "CUSTOMER123", "link": "link-1"},
|
customer_id="CUSTOMER123",
|
||||||
|
extra={"link": "link-1"},
|
||||||
success_url="https://lnbits.example/subscription-success",
|
success_url="https://lnbits.example/subscription-success",
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -921,10 +922,77 @@ async def test_revolut_wallet_create_subscription(settings: Settings):
|
|||||||
assert reference["wallet_id"] == "wallet_1"
|
assert reference["wallet_id"] == "wallet_1"
|
||||||
assert reference["tag"] == "gold"
|
assert reference["tag"] == "gold"
|
||||||
assert reference["memo"] == "Monthly Gold"
|
assert reference["memo"] == "Monthly Gold"
|
||||||
assert reference["extra"]["customer_id"] == "CUSTOMER123"
|
assert reference["extra"]["link"] == "link-1"
|
||||||
assert client.calls[1][0] == "/api/orders/ORDER123"
|
assert client.calls[1][0] == "/api/orders/ORDER123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_revolut_wallet_create_subscription_creates_customer(settings: Settings):
|
||||||
|
settings.revolut_api_endpoint = "https://sandbox-merchant.revolut.com"
|
||||||
|
settings.revolut_api_secret_key = "revolut-secret"
|
||||||
|
settings.revolut_api_version = "2026-04-20"
|
||||||
|
|
||||||
|
wallet = RevolutWallet()
|
||||||
|
client = MockHTTPClient(
|
||||||
|
[
|
||||||
|
MockHTTPResponse(json_data={"id": "CUSTOMER123"}),
|
||||||
|
MockHTTPResponse(
|
||||||
|
json_data={
|
||||||
|
"id": "SUBSCRIPTION123",
|
||||||
|
"setup_order_id": "ORDER123",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
MockHTTPResponse(
|
||||||
|
json_data={
|
||||||
|
"id": "ORDER123",
|
||||||
|
"checkout_url": "https://checkout.revolut.com/payment-link/sub_123",
|
||||||
|
}
|
||||||
|
),
|
||||||
|
]
|
||||||
|
)
|
||||||
|
wallet.client = client # type: ignore[assignment]
|
||||||
|
|
||||||
|
payment_options = FiatSubscriptionPaymentOptions(
|
||||||
|
wallet_id="wallet_1",
|
||||||
|
customer_email="customer@example.com",
|
||||||
|
)
|
||||||
|
|
||||||
|
response = await wallet.create_subscription(
|
||||||
|
"PLAN_VARIATION_123", 1, payment_options
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.ok is True
|
||||||
|
assert client.calls[0][0] == "/api/customers"
|
||||||
|
assert client.calls[0][1]["json"] == {"email": "customer@example.com"}
|
||||||
|
assert client.calls[1][0] == "/api/subscriptions"
|
||||||
|
assert client.calls[1][1]["json"]["customer_id"] == "CUSTOMER123"
|
||||||
|
assert client.calls[2][0] == "/api/orders/ORDER123"
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.mark.anyio
|
||||||
|
async def test_revolut_wallet_create_subscription_requires_customer(settings: Settings):
|
||||||
|
settings.revolut_api_endpoint = "https://sandbox-merchant.revolut.com"
|
||||||
|
settings.revolut_api_secret_key = "revolut-secret"
|
||||||
|
settings.revolut_api_version = "2026-04-20"
|
||||||
|
|
||||||
|
wallet = RevolutWallet()
|
||||||
|
client = MockHTTPClient([])
|
||||||
|
wallet.client = client # type: ignore[assignment]
|
||||||
|
|
||||||
|
payment_options = FiatSubscriptionPaymentOptions(wallet_id="wallet_1")
|
||||||
|
|
||||||
|
response = await wallet.create_subscription(
|
||||||
|
"PLAN_VARIATION_123", 1, payment_options
|
||||||
|
)
|
||||||
|
|
||||||
|
assert response.ok is False
|
||||||
|
assert (
|
||||||
|
response.error_message
|
||||||
|
== "Revolut subscriptions require customer_id or customer_email."
|
||||||
|
)
|
||||||
|
assert client.calls == []
|
||||||
|
|
||||||
|
|
||||||
@pytest.mark.anyio
|
@pytest.mark.anyio
|
||||||
async def test_revolut_wallet_cancel_subscription(settings: Settings):
|
async def test_revolut_wallet_cancel_subscription(settings: Settings):
|
||||||
settings.revolut_api_endpoint = "https://sandbox-merchant.revolut.com"
|
settings.revolut_api_endpoint = "https://sandbox-merchant.revolut.com"
|
||||||
|
|||||||
Reference in New Issue
Block a user