Skip to content
This repository was archived by the owner on Jun 13, 2025. It is now read-only.

Commit c5b388b

Browse files
authored
feat: Add Update Billing Address service function (#611)
1 parent ff7cbca commit c5b388b

File tree

5 files changed

+222
-0
lines changed

5 files changed

+222
-0
lines changed

api/internal/owner/views.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,17 @@ def update_email(self, request, *args, **kwargs):
106106
billing.update_email_address(owner, new_email)
107107
return Response(self.get_serializer(owner).data)
108108

109+
@action(detail=False, methods=["patch"])
110+
@stripe_safe
111+
def update_billing_address(self, request, *args, **kwargs):
112+
billing_address = request.data.get("billing_address")
113+
if not billing_address:
114+
raise ValidationError(detail="No billing_address sent")
115+
owner = self.get_object()
116+
billing = BillingService(requesting_user=request.current_owner)
117+
billing.update_billing_address(owner, billing_address)
118+
return Response(self.get_serializer(owner).data)
119+
109120

110121
class UsersOrderingFilter(filters.OrderingFilter):
111122
def get_valid_fields(self, queryset, view, context=None):
Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
interactions:
2+
- request:
3+
body: null
4+
headers:
5+
Accept:
6+
- '*/*'
7+
Accept-Encoding:
8+
- gzip, deflate
9+
Connection:
10+
- keep-alive
11+
Stripe-Version:
12+
- '2024-04-10'
13+
User-Agent:
14+
- Stripe/v1 PythonBindings/9.6.0
15+
X-Stripe-Client-User-Agent:
16+
- '{"bindings_version": "9.6.0", "lang": "python", "publisher": "stripe", "httplib":
17+
"requests", "lang_version": "3.12.3", "platform": "Linux-6.6.31-linuxkit-aarch64-with-glibc2.36",
18+
"uname": "Linux b0efe3849169 6.6.31-linuxkit #1 SMP Thu May 23 08:36:57 UTC
19+
2024 aarch64 "}'
20+
method: GET
21+
uri: https://api.stripe.com/v1/subscriptions/djfos?expand%5B0%5D=latest_invoice&expand%5B1%5D=customer&expand%5B2%5D=customer.invoice_settings.default_payment_method
22+
response:
23+
body:
24+
string: "{\n \"error\": {\n \"code\": \"resource_missing\",\n \"doc_url\":
25+
\"https://stripe.com/docs/error-codes/resource-missing\",\n \"message\":
26+
\"No such subscription: 'djfos'\",\n \"param\": \"id\",\n \"request_log_url\":
27+
\"https://dashboard.stripe.com/test/logs/req_4POjCjRMnbE5Wg?t=1718056644\",\n
28+
\ \"type\": \"invalid_request_error\"\n }\n}\n"
29+
headers:
30+
Access-Control-Allow-Credentials:
31+
- 'true'
32+
Access-Control-Allow-Methods:
33+
- GET,HEAD,PUT,PATCH,POST,DELETE
34+
Access-Control-Allow-Origin:
35+
- '*'
36+
Access-Control-Expose-Headers:
37+
- Request-Id, Stripe-Manage-Version, Stripe-Should-Retry, X-Stripe-External-Auth-Required,
38+
X-Stripe-Privileged-Session-Required
39+
Access-Control-Max-Age:
40+
- '300'
41+
Cache-Control:
42+
- no-cache, no-store
43+
Connection:
44+
- keep-alive
45+
Content-Length:
46+
- '324'
47+
Content-Security-Policy:
48+
- report-uri https://q.stripe.com/csp-report?p=v1%2Fsubscriptions%2F%3Asubscription_exposed_id;
49+
block-all-mixed-content; default-src 'none'; base-uri 'none'; form-action
50+
'none'; frame-ancestors 'none'; img-src 'self'; script-src 'self' 'report-sample';
51+
style-src 'self'
52+
Content-Type:
53+
- application/json
54+
Cross-Origin-Opener-Policy-Report-Only:
55+
- same-origin; report-to="coop"
56+
Date:
57+
- Mon, 10 Jun 2024 21:57:24 GMT
58+
Report-To:
59+
- '{"group":"coop","max_age":8640,"endpoints":[{"url":"https://q.stripe.com/coop-report?s=billing-api-srv"}],"include_subdomains":true}'
60+
Reporting-Endpoints:
61+
- coop="https://q.stripe.com/coop-report?s=billing-api-srv"
62+
Request-Id:
63+
- req_4POjCjRMnbE5Wg
64+
Server:
65+
- nginx
66+
Strict-Transport-Security:
67+
- max-age=63072000; includeSubDomains; preload
68+
Stripe-Version:
69+
- '2024-04-10'
70+
Vary:
71+
- Origin
72+
X-Content-Type-Options:
73+
- nosniff
74+
X-Stripe-Routing-Context-Priority-Tier:
75+
- api-testmode
76+
status:
77+
code: 404
78+
message: Not Found
79+
version: 1

api/internal/tests/views/test_account_viewset.py

Lines changed: 64 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1068,6 +1068,70 @@ def test_update_email_address(self, modify_customer_mock, retrieve_mock):
10681068
self.current_owner.stripe_customer_id, email=new_email
10691069
)
10701070

1071+
def test_update_billing_address_without_body(self):
1072+
kwargs = {
1073+
"service": self.current_owner.service,
1074+
"owner_username": self.current_owner.username,
1075+
}
1076+
url = reverse("account_details-update-billing-address", kwargs=kwargs)
1077+
response = self.client.patch(url, format="json")
1078+
assert response.status_code == status.HTTP_400_BAD_REQUEST
1079+
1080+
@patch("services.billing.StripeService.update_billing_address")
1081+
def test_update_billing_address_handles_stripe_error(self, stripe_mock):
1082+
code, message = 402, "Oops, nope"
1083+
self.current_owner.stripe_customer_id = "flsoe"
1084+
self.current_owner.stripe_subscription_id = "djfos"
1085+
self.current_owner.save()
1086+
1087+
stripe_mock.side_effect = StripeError(message=message, http_status=code)
1088+
1089+
billing_address = {
1090+
"line_1": "45 Fremont St.",
1091+
"line_2": "",
1092+
"city": "San Francisco",
1093+
"state": "CA",
1094+
"country": "US",
1095+
"postal_code": "94105",
1096+
}
1097+
kwargs = {
1098+
"service": self.current_owner.service,
1099+
"owner_username": self.current_owner.username,
1100+
}
1101+
data = {"billing_address": billing_address}
1102+
url = reverse("account_details-update-billing-address", kwargs=kwargs)
1103+
response = self.client.patch(url, data=data, format="json")
1104+
assert response.status_code == code
1105+
assert response.data["detail"] == message
1106+
1107+
@patch("services.billing.stripe.Subscription.retrieve")
1108+
@patch("services.billing.stripe.Customer.modify")
1109+
def test_update_billing_address(self, modify_customer_mock, retrieve_mock):
1110+
self.current_owner.stripe_customer_id = "flsoe"
1111+
self.current_owner.stripe_subscription_id = "djfos"
1112+
self.current_owner.save()
1113+
1114+
billing_address = {
1115+
"line_1": "45 Fremont St.",
1116+
"line_2": "",
1117+
"city": "San Francisco",
1118+
"state": "CA",
1119+
"country": "US",
1120+
"postal_code": "94105",
1121+
}
1122+
kwargs = {
1123+
"service": self.current_owner.service,
1124+
"owner_username": self.current_owner.username,
1125+
}
1126+
data = {"billing_address": billing_address}
1127+
url = reverse("account_details-update-billing-address", kwargs=kwargs)
1128+
response = self.client.patch(url, data=data, format="json")
1129+
assert response.status_code == status.HTTP_200_OK
1130+
1131+
modify_customer_mock.assert_called_once_with(
1132+
self.current_owner.stripe_customer_id, address=billing_address
1133+
)
1134+
10711135
@patch("api.shared.permissions.get_provider")
10721136
def test_update_without_admin_permissions_returns_404(self, get_provider_mock):
10731137
get_provider_mock.return_value = GetAdminProviderAdapter()

services/billing.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -429,6 +429,19 @@ def update_email_address(self, owner: Owner, email_address: str):
429429
f"Stripe successfully updated email address for owner {owner.ownerid} by user #{self.requesting_user.ownerid}"
430430
)
431431

432+
@_log_stripe_error
433+
def update_billing_address(self, owner: Owner, billing_address):
434+
log.info(f"Stripe update billing address for owner {owner.ownerid}")
435+
if owner.stripe_subscription_id is None:
436+
log.info(
437+
f"stripe_subscription_id is None, cannot update billing address for owner {owner.ownerid}"
438+
)
439+
return None
440+
stripe.Customer.modify(owner.stripe_customer_id, address=billing_address)
441+
log.info(
442+
f"Stripe successfully updated billing address for owner {owner.ownerid} by user #{self.requesting_user.ownerid}"
443+
)
444+
432445
@_log_stripe_error
433446
def apply_cancellation_discount(self, owner: Owner):
434447
if owner.stripe_subscription_id is None:
@@ -494,6 +507,9 @@ def update_payment_method(self, owner, payment_method):
494507
def update_email_address(self, owner, email_address):
495508
pass
496509

510+
def update_billing_address(self, owner, billing_address):
511+
pass
512+
497513
def get_schedule(self, owner):
498514
pass
499515

@@ -570,5 +586,13 @@ def update_email_address(self, owner: Owner, email_address: str):
570586
"""
571587
return self.payment_service.update_email_address(owner, email_address)
572588

589+
def update_billing_address(self, owner: Owner, billing_address):
590+
"""
591+
Takes an owner and a billing address. Try to update the owner's billing address
592+
to the address passed in. Address should be validated via stripe component prior
593+
to hitting this service method. Return None if invalid.
594+
"""
595+
return self.payment_service.update_billing_address(owner, billing_address)
596+
573597
def apply_cancellation_discount(self, owner: Owner):
574598
return self.payment_service.apply_cancellation_discount(owner)

services/tests/test_billing.py

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1140,6 +1140,50 @@ def test_update_email_address(self, modify_customer_mock):
11401140
self.stripe.update_email_address(owner, "[email protected]")
11411141
modify_customer_mock.assert_called_once_with(customer_id, email=email)
11421142

1143+
def test_update_billing_address_with_invalid_email(self):
1144+
owner = OwnerFactory(stripe_subscription_id=None)
1145+
assert self.stripe.update_billing_address(owner, "gabagool") == None
1146+
1147+
def test_update_billing_address_when_no_subscription(self):
1148+
owner = OwnerFactory(stripe_subscription_id=None)
1149+
assert (
1150+
self.stripe.update_billing_address(
1151+
owner,
1152+
billing_address={
1153+
"line_1": "45 Fremont St.",
1154+
"line_2": "",
1155+
"city": "San Francisco",
1156+
"state": "CA",
1157+
"country": "US",
1158+
"postal_code": "94105",
1159+
},
1160+
)
1161+
== None
1162+
)
1163+
1164+
@patch("services.billing.stripe.Customer.modify")
1165+
def test_update_billing_address(self, modify_customer_mock):
1166+
subscription_id = "sub_abc"
1167+
customer_id = "cus_abc"
1168+
owner = OwnerFactory(
1169+
stripe_subscription_id=subscription_id, stripe_customer_id=customer_id
1170+
)
1171+
billing_address = {
1172+
"line_1": "45 Fremont St.",
1173+
"line_2": "",
1174+
"city": "San Francisco",
1175+
"state": "CA",
1176+
"country": "US",
1177+
"postal_code": "94105",
1178+
}
1179+
self.stripe.update_billing_address(
1180+
owner,
1181+
billing_address=billing_address,
1182+
)
1183+
modify_customer_mock.assert_called_once_with(
1184+
customer_id, address=billing_address
1185+
)
1186+
11431187
@patch("services.billing.stripe.Invoice.retrieve")
11441188
def test_get_invoice_not_found(self, retrieve_invoice_mock):
11451189
invoice_id = "abc"

0 commit comments

Comments
 (0)