svc-infra 0.1.579__py3-none-any.whl → 0.1.580__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.

Potentially problematic release.


This version of svc-infra might be problematic. Click here for more details.

@@ -11,21 +11,27 @@ from ..schemas import (
11
11
  IntentOut,
12
12
  InvoiceCreateIn,
13
13
  InvoiceLineItemIn,
14
+ InvoiceLineItemOut,
14
15
  InvoiceOut,
15
16
  PaymentMethodAttachIn,
16
17
  PaymentMethodOut,
18
+ PaymentMethodUpdateIn,
17
19
  PayoutOut,
18
20
  PriceCreateIn,
19
21
  PriceOut,
22
+ PriceUpdateIn,
20
23
  ProductCreateIn,
21
24
  ProductOut,
25
+ ProductUpdateIn,
22
26
  RefundIn,
27
+ RefundOut,
23
28
  SetupIntentCreateIn,
24
29
  SetupIntentOut,
25
30
  SubscriptionCreateIn,
26
31
  SubscriptionOut,
27
32
  SubscriptionUpdateIn,
28
33
  UsageRecordIn,
34
+ UsageRecordListFilter,
29
35
  UsageRecordOut,
30
36
  )
31
37
 
@@ -36,9 +42,6 @@ class ProviderAdapter(Protocol):
36
42
  async def ensure_customer(self, data: CustomerUpsertIn) -> CustomerOut:
37
43
  pass
38
44
 
39
- async def get_customer(self, provider_customer_id: str) -> Optional[CustomerOut]:
40
- pass
41
-
42
45
  async def attach_payment_method(self, data: PaymentMethodAttachIn) -> PaymentMethodOut:
43
46
  pass
44
47
 
@@ -180,3 +183,88 @@ class ProviderAdapter(Protocol):
180
183
 
181
184
  async def get_payout(self, provider_payout_id: str) -> PayoutOut:
182
185
  pass
186
+
187
+ # --- Customers ---
188
+ async def list_customers(
189
+ self, *, provider: str | None, user_id: str | None, limit: int, cursor: str | None
190
+ ) -> tuple[list[CustomerOut], str | None]:
191
+ """Optional: if not implemented, the service will list from local DB."""
192
+ pass
193
+
194
+ async def get_customer(self, provider_customer_id: str) -> Optional[CustomerOut]:
195
+ pass
196
+
197
+ # --- Products / Prices ---
198
+ async def get_product(self, provider_product_id: str) -> ProductOut:
199
+ pass
200
+
201
+ async def list_products(
202
+ self, *, active: bool | None, limit: int, cursor: str | None
203
+ ) -> tuple[list[ProductOut], str | None]:
204
+ pass
205
+
206
+ async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
207
+ pass
208
+
209
+ async def get_price(self, provider_price_id: str) -> PriceOut:
210
+ pass
211
+
212
+ async def list_prices(
213
+ self,
214
+ *,
215
+ provider_product_id: str | None,
216
+ active: bool | None,
217
+ limit: int,
218
+ cursor: str | None,
219
+ ) -> tuple[list[PriceOut], str | None]:
220
+ pass
221
+
222
+ async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
223
+ pass
224
+
225
+ # --- Subscriptions ---
226
+ async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
227
+ pass
228
+
229
+ async def list_subscriptions(
230
+ self,
231
+ *,
232
+ customer_provider_id: str | None,
233
+ status: str | None,
234
+ limit: int,
235
+ cursor: str | None,
236
+ ) -> tuple[list[SubscriptionOut], str | None]:
237
+ pass
238
+
239
+ # --- Payment Method (single + update) ---
240
+ async def get_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
241
+ pass
242
+
243
+ async def update_payment_method(
244
+ self, provider_method_id: str, data: PaymentMethodUpdateIn
245
+ ) -> PaymentMethodOut:
246
+ pass
247
+
248
+ # --- Refunds list/get ---
249
+ async def list_refunds(
250
+ self, *, provider_payment_intent_id: str | None, limit: int, cursor: str | None
251
+ ) -> tuple[list[RefundOut], str | None]:
252
+ pass
253
+
254
+ async def get_refund(self, provider_refund_id: str) -> RefundOut:
255
+ pass
256
+
257
+ # --- Invoice line items list ---
258
+ async def list_invoice_line_items(
259
+ self, provider_invoice_id: str, *, limit: int, cursor: str | None
260
+ ) -> tuple[list[InvoiceLineItemOut], str | None]:
261
+ pass
262
+
263
+ # --- Usage records list/get ---
264
+ async def list_usage_records(
265
+ self, f: UsageRecordListFilter
266
+ ) -> tuple[list[UsageRecordOut], str | None]:
267
+ pass
268
+
269
+ async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
270
+ pass
@@ -268,3 +268,61 @@ class UsageRecordOut(BaseModel):
268
268
  timestamp: Optional[int] = None
269
269
  subscription_item: Optional[str] = None
270
270
  provider_price_id: Optional[str] = None
271
+
272
+
273
+ # -------- Customers list filter ----------
274
+ class CustomersListFilter(BaseModel):
275
+ provider: Optional[str] = None
276
+ user_id: Optional[str] = None
277
+ limit: Optional[int] = Field(default=50, ge=1, le=200)
278
+ cursor: Optional[str] = None # we’ll paginate on provider_customer_id asc
279
+
280
+
281
+ # -------- Products / Prices updates ----------
282
+ class ProductUpdateIn(BaseModel):
283
+ name: Optional[str] = None
284
+ active: Optional[bool] = None
285
+
286
+
287
+ class PriceUpdateIn(BaseModel):
288
+ active: Optional[bool] = None
289
+
290
+
291
+ # -------- Payment Method update ----------
292
+ class PaymentMethodUpdateIn(BaseModel):
293
+ # keep minimal + commonly supported card fields
294
+ name: Optional[str] = None
295
+ exp_month: Optional[int] = None
296
+ exp_year: Optional[int] = None
297
+ # extend here later with address fields (line1, city, etc.)
298
+
299
+
300
+ # -------- Refunds (list/get) ----------
301
+ class RefundOut(BaseModel):
302
+ id: str
303
+ provider: str
304
+ provider_refund_id: str
305
+ provider_payment_intent_id: Optional[str] = None
306
+ amount: AmountMinor
307
+ currency: Currency
308
+ status: str
309
+ reason: Optional[str] = None
310
+ created_at: Optional[str] = None
311
+
312
+
313
+ # -------- Invoice line items (list) ----------
314
+ class InvoiceLineItemOut(BaseModel):
315
+ id: str
316
+ description: Optional[str] = None
317
+ amount: AmountMinor
318
+ currency: Currency
319
+ quantity: Optional[int] = 1
320
+ provider_price_id: Optional[str] = None
321
+
322
+
323
+ # -------- Usage records list/get ----------
324
+ class UsageRecordListFilter(BaseModel):
325
+ subscription_item: Optional[str] = None
326
+ provider_price_id: Optional[str] = None
327
+ limit: Optional[int] = Field(default=50, ge=1, le=200)
328
+ cursor: Optional[str] = None
@@ -24,6 +24,7 @@ from .schemas import (
24
24
  BalanceSnapshotOut,
25
25
  CaptureIn,
26
26
  CustomerOut,
27
+ CustomersListFilter,
27
28
  CustomerUpsertIn,
28
29
  DisputeOut,
29
30
  IntentCreateIn,
@@ -31,16 +32,21 @@ from .schemas import (
31
32
  IntentOut,
32
33
  InvoiceCreateIn,
33
34
  InvoiceLineItemIn,
35
+ InvoiceLineItemOut,
34
36
  InvoiceOut,
35
37
  InvoicesListFilter,
36
38
  PaymentMethodAttachIn,
37
39
  PaymentMethodOut,
40
+ PaymentMethodUpdateIn,
38
41
  PayoutOut,
39
42
  PriceCreateIn,
40
43
  PriceOut,
44
+ PriceUpdateIn,
41
45
  ProductCreateIn,
42
46
  ProductOut,
47
+ ProductUpdateIn,
43
48
  RefundIn,
49
+ RefundOut,
44
50
  SetupIntentCreateIn,
45
51
  SetupIntentOut,
46
52
  StatementRow,
@@ -48,6 +54,7 @@ from .schemas import (
48
54
  SubscriptionOut,
49
55
  SubscriptionUpdateIn,
50
56
  UsageRecordIn,
57
+ UsageRecordListFilter,
51
58
  UsageRecordOut,
52
59
  )
53
60
  from .settings import get_payments_settings
@@ -624,3 +631,176 @@ class PaymentsService:
624
631
  await self._dispatch_event(ev.provider, ev.payload_json)
625
632
 
626
633
  return len(rows)
634
+
635
+ # ---- Customers ----
636
+ async def list_customers(self, f: CustomersListFilter) -> tuple[list[CustomerOut], str | None]:
637
+ adapter = self._get_adapter()
638
+ try:
639
+ return await adapter.list_customers(
640
+ provider=f.provider, user_id=f.user_id, limit=f.limit or 50, cursor=f.cursor
641
+ )
642
+ except NotImplementedError:
643
+ # Fallback to local DB listing
644
+ q = select(PayCustomer).order_by(PayCustomer.provider_customer_id.asc())
645
+ if f.provider:
646
+ q = q.where(PayCustomer.provider == f.provider)
647
+ if f.user_id:
648
+ q = q.where(PayCustomer.user_id == f.user_id)
649
+ rows = (await self.session.execute(q)).scalars().all()
650
+ # simple cursor by provider_customer_id; production can optimize
651
+ next_cursor = None
652
+ if f.limit and len(rows) > f.limit:
653
+ rows = rows[: f.limit]
654
+ next_cursor = rows[-1].provider_customer_id
655
+ return (
656
+ [
657
+ CustomerOut(
658
+ id=r.id,
659
+ provider=r.provider,
660
+ provider_customer_id=r.provider_customer_id,
661
+ email=None,
662
+ name=None,
663
+ )
664
+ for r in rows
665
+ ],
666
+ next_cursor,
667
+ )
668
+
669
+ async def get_customer(self, provider_customer_id: str) -> CustomerOut:
670
+ adapter = self._get_adapter()
671
+ out = await adapter.get_customer(provider_customer_id)
672
+ if out is None:
673
+ raise RuntimeError("Customer not found")
674
+ # upsert locally
675
+ row = await self.session.scalar(
676
+ select(PayCustomer).where(PayCustomer.provider_customer_id == provider_customer_id)
677
+ )
678
+ if not row:
679
+ self.session.add(
680
+ PayCustomer(
681
+ provider=out.provider,
682
+ provider_customer_id=out.provider_customer_id,
683
+ user_id=None,
684
+ tenant_id="",
685
+ )
686
+ )
687
+ return out
688
+
689
+ # ---- Products / Prices ----
690
+ async def get_product(self, provider_product_id: str) -> ProductOut:
691
+ return await self._get_adapter().get_product(provider_product_id)
692
+
693
+ async def list_products(
694
+ self, *, active: bool | None, limit: int, cursor: str | None
695
+ ) -> tuple[list[ProductOut], str | None]:
696
+ return await self._get_adapter().list_products(active=active, limit=limit, cursor=cursor)
697
+
698
+ async def update_product(self, provider_product_id: str, data: ProductUpdateIn) -> ProductOut:
699
+ out = await self._get_adapter().update_product(provider_product_id, data)
700
+ # reflect DB
701
+ row = await self.session.scalar(
702
+ select(PayProduct).where(PayProduct.provider_product_id == provider_product_id)
703
+ )
704
+ if row:
705
+ if data.name is not None:
706
+ row.name = data.name
707
+ if data.active is not None:
708
+ row.active = data.active
709
+ return out
710
+
711
+ async def get_price(self, provider_price_id: str) -> PriceOut:
712
+ return await self._get_adapter().get_price(provider_price_id)
713
+
714
+ async def list_prices(
715
+ self,
716
+ *,
717
+ provider_product_id: str | None,
718
+ active: bool | None,
719
+ limit: int,
720
+ cursor: str | None,
721
+ ) -> tuple[list[PriceOut], str | None]:
722
+ return await self._get_adapter().list_prices(
723
+ provider_product_id=provider_product_id, active=active, limit=limit, cursor=cursor
724
+ )
725
+
726
+ async def update_price(self, provider_price_id: str, data: PriceUpdateIn) -> PriceOut:
727
+ out = await self._get_adapter().update_price(provider_price_id, data)
728
+ row = await self.session.scalar(
729
+ select(PayPrice).where(PayPrice.provider_price_id == provider_price_id)
730
+ )
731
+ if row and data.active is not None:
732
+ row.active = data.active
733
+ return out
734
+
735
+ # ---- Subscriptions ----
736
+ async def get_subscription(self, provider_subscription_id: str) -> SubscriptionOut:
737
+ return await self._get_adapter().get_subscription(provider_subscription_id)
738
+
739
+ async def list_subscriptions(
740
+ self,
741
+ *,
742
+ customer_provider_id: str | None,
743
+ status: str | None,
744
+ limit: int,
745
+ cursor: str | None,
746
+ ) -> tuple[list[SubscriptionOut], str | None]:
747
+ return await self._get_adapter().list_subscriptions(
748
+ customer_provider_id=customer_provider_id, status=status, limit=limit, cursor=cursor
749
+ )
750
+
751
+ # ---- Payment Methods (get/update) ----
752
+ async def get_payment_method(self, provider_method_id: str) -> PaymentMethodOut:
753
+ return await self._get_adapter().get_payment_method(provider_method_id)
754
+
755
+ async def update_payment_method(
756
+ self, provider_method_id: str, data: PaymentMethodUpdateIn
757
+ ) -> PaymentMethodOut:
758
+ out = await self._get_adapter().update_payment_method(provider_method_id, data)
759
+ row = await self.session.scalar(
760
+ select(PayPaymentMethod).where(
761
+ PayPaymentMethod.provider_method_id == provider_method_id
762
+ )
763
+ )
764
+ if row:
765
+ if data.name is not None:
766
+ pass # keep local-only if/when you add column
767
+ if data.exp_month is not None:
768
+ row.exp_month = data.exp_month
769
+ if data.exp_year is not None:
770
+ row.exp_year = data.exp_year
771
+ return out
772
+
773
+ # ---- Refunds list/get ----
774
+ async def list_refunds(
775
+ self, *, provider_payment_intent_id: str | None, limit: int, cursor: str | None
776
+ ) -> tuple[list[RefundOut], str | None]:
777
+ return await self._get_adapter().list_refunds(
778
+ provider_payment_intent_id=provider_payment_intent_id, limit=limit, cursor=cursor
779
+ )
780
+
781
+ async def get_refund(self, provider_refund_id: str) -> RefundOut:
782
+ return await self._get_adapter().get_refund(provider_refund_id)
783
+
784
+ # ---- Invoice line items list ----
785
+ async def list_invoice_line_items(
786
+ self, provider_invoice_id: str, *, limit: int, cursor: str | None
787
+ ) -> tuple[list[InvoiceLineItemOut], str | None]:
788
+ return await self._get_adapter().list_invoice_line_items(
789
+ provider_invoice_id, limit=limit, cursor=cursor
790
+ )
791
+
792
+ # ---- Usage records list/get ----
793
+ async def list_usage_records(
794
+ self, f: UsageRecordListFilter
795
+ ) -> tuple[list[UsageRecordOut], str | None]:
796
+ return await self._get_adapter().list_usage_records(f)
797
+
798
+ async def get_usage_record(self, usage_record_id: str) -> UsageRecordOut:
799
+ return await self._get_adapter().get_usage_record(usage_record_id)
800
+
801
+ async def delete_invoice_line_item(
802
+ self, provider_invoice_id: str, provider_line_item_id: str
803
+ ) -> InvoiceOut:
804
+ return await self._get_adapter().delete_invoice_line_item(
805
+ provider_invoice_id, provider_line_item_id
806
+ )
@@ -9,6 +9,7 @@ from svc_infra.apf_payments.schemas import (
9
9
  BalanceSnapshotOut,
10
10
  CaptureIn,
11
11
  CustomerOut,
12
+ CustomersListFilter,
12
13
  CustomerUpsertIn,
13
14
  DisputeOut,
14
15
  IntentCreateIn,
@@ -16,16 +17,21 @@ from svc_infra.apf_payments.schemas import (
16
17
  IntentOut,
17
18
  InvoiceCreateIn,
18
19
  InvoiceLineItemIn,
20
+ InvoiceLineItemOut,
19
21
  InvoiceOut,
20
22
  InvoicesListFilter,
21
23
  PaymentMethodAttachIn,
22
24
  PaymentMethodOut,
25
+ PaymentMethodUpdateIn,
23
26
  PayoutOut,
24
27
  PriceCreateIn,
25
28
  PriceOut,
29
+ PriceUpdateIn,
26
30
  ProductCreateIn,
27
31
  ProductOut,
32
+ ProductUpdateIn,
28
33
  RefundIn,
34
+ RefundOut,
29
35
  SetupIntentCreateIn,
30
36
  SetupIntentOut,
31
37
  StatementRow,
@@ -34,6 +40,7 @@ from svc_infra.apf_payments.schemas import (
34
40
  SubscriptionUpdateIn,
35
41
  TransactionRow,
36
42
  UsageRecordIn,
43
+ UsageRecordListFilter,
37
44
  UsageRecordOut,
38
45
  WebhookAckOut,
39
46
  WebhookReplayOut,
@@ -657,6 +664,288 @@ def build_payments_routers(prefix: str = "/payments") -> list[DualAPIRouter]:
657
664
  await svc.session.flush()
658
665
  return {"replayed": count}
659
666
 
667
+ # ===== Customers: list/get =====
668
+ @prot.get(
669
+ "/customers",
670
+ response_model=Paginated[CustomerOut],
671
+ name="payments_list_customers",
672
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
673
+ )
674
+ async def list_customers_endpoint(
675
+ provider: Optional[str] = None,
676
+ user_id: Optional[str] = None,
677
+ svc: PaymentsService = Depends(get_service),
678
+ ):
679
+ ctx = use_pagination()
680
+ items, next_cursor = await svc.list_customers(
681
+ CustomersListFilter(
682
+ provider=provider, user_id=user_id, limit=ctx.limit, cursor=ctx.cursor
683
+ )
684
+ )
685
+ return ctx.wrap(items, next_cursor=next_cursor)
686
+
687
+ @prot.get(
688
+ "/customers/{provider_customer_id}",
689
+ response_model=CustomerOut,
690
+ name="payments_get_customer",
691
+ )
692
+ async def get_customer_endpoint(
693
+ provider_customer_id: str, svc: PaymentsService = Depends(get_service)
694
+ ):
695
+ return await svc.get_customer(provider_customer_id)
696
+
697
+ # ===== Payment Methods: get/update =====
698
+ @prot.get(
699
+ "/methods/{provider_method_id}",
700
+ response_model=PaymentMethodOut,
701
+ name="payments_get_method",
702
+ )
703
+ async def get_method(provider_method_id: str, svc: PaymentsService = Depends(get_service)):
704
+ return await svc.get_payment_method(provider_method_id)
705
+
706
+ @prot.post(
707
+ "/methods/{provider_method_id}",
708
+ response_model=PaymentMethodOut,
709
+ name="payments_update_method",
710
+ dependencies=[Depends(require_idempotency_key)],
711
+ )
712
+ async def update_method(
713
+ provider_method_id: str,
714
+ data: PaymentMethodUpdateIn,
715
+ svc: PaymentsService = Depends(get_service),
716
+ ):
717
+ out = await svc.update_payment_method(provider_method_id, data)
718
+ await svc.session.flush()
719
+ return out
720
+
721
+ # ===== Products: get/list/update (archive via active=False) =====
722
+ @svc.get(
723
+ "/products/{provider_product_id}",
724
+ response_model=ProductOut,
725
+ name="payments_get_product",
726
+ )
727
+ async def get_product_endpoint(
728
+ provider_product_id: str, svc: PaymentsService = Depends(get_service)
729
+ ):
730
+ return await svc.get_product(provider_product_id)
731
+
732
+ @svc.get(
733
+ "/products",
734
+ response_model=Paginated[ProductOut],
735
+ name="payments_list_products",
736
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
737
+ )
738
+ async def list_products_endpoint(
739
+ active: Optional[bool] = None,
740
+ svc: PaymentsService = Depends(get_service),
741
+ ):
742
+ ctx = use_pagination()
743
+ items, next_cursor = await svc.list_products(
744
+ active=active, limit=ctx.limit, cursor=ctx.cursor
745
+ )
746
+ return ctx.wrap(items, next_cursor=next_cursor)
747
+
748
+ @svc.post(
749
+ "/products/{provider_product_id}",
750
+ response_model=ProductOut,
751
+ name="payments_update_product",
752
+ dependencies=[Depends(require_idempotency_key)],
753
+ )
754
+ async def update_product_endpoint(
755
+ provider_product_id: str,
756
+ data: ProductUpdateIn,
757
+ svc: PaymentsService = Depends(get_service),
758
+ ):
759
+ out = await svc.update_product(provider_product_id, data)
760
+ await svc.session.flush()
761
+ return out
762
+
763
+ # ===== Prices: get/list/update (active toggle) =====
764
+ @svc.get(
765
+ "/prices/{provider_price_id}",
766
+ response_model=PriceOut,
767
+ name="payments_get_price",
768
+ )
769
+ async def get_price_endpoint(
770
+ provider_price_id: str, svc: PaymentsService = Depends(get_service)
771
+ ):
772
+ return await svc.get_price(provider_price_id)
773
+
774
+ @svc.get(
775
+ "/prices",
776
+ response_model=Paginated[PriceOut],
777
+ name="payments_list_prices",
778
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
779
+ )
780
+ async def list_prices_endpoint(
781
+ provider_product_id: Optional[str] = None,
782
+ active: Optional[bool] = None,
783
+ svc: PaymentsService = Depends(get_service),
784
+ ):
785
+ ctx = use_pagination()
786
+ items, next_cursor = await svc.list_prices(
787
+ provider_product_id=provider_product_id,
788
+ active=active,
789
+ limit=ctx.limit,
790
+ cursor=ctx.cursor,
791
+ )
792
+ return ctx.wrap(items, next_cursor=next_cursor)
793
+
794
+ @svc.post(
795
+ "/prices/{provider_price_id}",
796
+ response_model=PriceOut,
797
+ name="payments_update_price",
798
+ dependencies=[Depends(require_idempotency_key)],
799
+ )
800
+ async def update_price_endpoint(
801
+ provider_price_id: str,
802
+ data: PriceUpdateIn,
803
+ svc: PaymentsService = Depends(get_service),
804
+ ):
805
+ out = await svc.update_price(provider_price_id, data)
806
+ await svc.session.flush()
807
+ return out
808
+
809
+ # ===== Subscriptions: get/list =====
810
+ @prot.get(
811
+ "/subscriptions/{provider_subscription_id}",
812
+ response_model=SubscriptionOut,
813
+ name="payments_get_subscription",
814
+ )
815
+ async def get_subscription_endpoint(
816
+ provider_subscription_id: str, svc: PaymentsService = Depends(get_service)
817
+ ):
818
+ return await svc.get_subscription(provider_subscription_id)
819
+
820
+ @prot.get(
821
+ "/subscriptions",
822
+ response_model=Paginated[SubscriptionOut],
823
+ name="payments_list_subscriptions",
824
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
825
+ )
826
+ async def list_subscriptions_endpoint(
827
+ customer_provider_id: Optional[str] = None,
828
+ status: Optional[str] = None,
829
+ svc: PaymentsService = Depends(get_service),
830
+ ):
831
+ ctx = use_pagination()
832
+ items, next_cursor = await svc.list_subscriptions(
833
+ customer_provider_id=customer_provider_id,
834
+ status=status,
835
+ limit=ctx.limit,
836
+ cursor=ctx.cursor,
837
+ )
838
+ return ctx.wrap(items, next_cursor=next_cursor)
839
+
840
+ # ===== Invoices: list line items =====
841
+ @prot.get(
842
+ "/invoices/{provider_invoice_id}/lines",
843
+ response_model=Paginated[InvoiceLineItemOut],
844
+ name="payments_list_invoice_line_items",
845
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
846
+ )
847
+ async def list_invoice_lines_endpoint(
848
+ provider_invoice_id: str,
849
+ svc: PaymentsService = Depends(get_service),
850
+ ):
851
+ ctx = use_pagination()
852
+ items, next_cursor = await svc.list_invoice_line_items(
853
+ provider_invoice_id, limit=ctx.limit, cursor=ctx.cursor
854
+ )
855
+ return ctx.wrap(items, next_cursor=next_cursor)
856
+
857
+ # ===== Refunds: list/get =====
858
+ @prot.get(
859
+ "/refunds",
860
+ response_model=Paginated[RefundOut],
861
+ name="payments_list_refunds",
862
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
863
+ )
864
+ async def list_refunds_endpoint(
865
+ provider_payment_intent_id: Optional[str] = None,
866
+ svc: PaymentsService = Depends(get_service),
867
+ ):
868
+ ctx = use_pagination()
869
+ items, next_cursor = await svc.list_refunds(
870
+ provider_payment_intent_id=provider_payment_intent_id,
871
+ limit=ctx.limit,
872
+ cursor=ctx.cursor,
873
+ )
874
+ return ctx.wrap(items, next_cursor=next_cursor)
875
+
876
+ @prot.get(
877
+ "/refunds/{provider_refund_id}",
878
+ response_model=RefundOut,
879
+ name="payments_get_refund",
880
+ )
881
+ async def get_refund_endpoint(
882
+ provider_refund_id: str, svc: PaymentsService = Depends(get_service)
883
+ ):
884
+ return await svc.get_refund(provider_refund_id)
885
+
886
+ # ===== Usage Records: list/get =====
887
+ @prot.get(
888
+ "/usage_records",
889
+ response_model=Paginated[UsageRecordOut],
890
+ name="payments_list_usage_records",
891
+ dependencies=[Depends(cursor_pager(default_limit=50, max_limit=200))],
892
+ )
893
+ async def list_usage_records_endpoint(
894
+ subscription_item: Optional[str] = None,
895
+ provider_price_id: Optional[str] = None,
896
+ svc: PaymentsService = Depends(get_service),
897
+ ):
898
+ ctx = use_pagination()
899
+ items, next_cursor = await svc.list_usage_records(
900
+ UsageRecordListFilter(
901
+ subscription_item=subscription_item,
902
+ provider_price_id=provider_price_id,
903
+ limit=ctx.limit,
904
+ cursor=ctx.cursor,
905
+ )
906
+ )
907
+ return ctx.wrap(items, next_cursor=next_cursor)
908
+
909
+ @prot.get(
910
+ "/usage_records/{usage_record_id}",
911
+ response_model=UsageRecordOut,
912
+ name="payments_get_usage_record",
913
+ )
914
+ async def get_usage_record_endpoint(
915
+ usage_record_id: str, svc: PaymentsService = Depends(get_service)
916
+ ):
917
+ return await svc.get_usage_record(usage_record_id)
918
+
919
+ # --- Invoices: delete line item ---
920
+ @prot.delete(
921
+ "/invoices/{provider_invoice_id}/lines/{provider_line_item_id}",
922
+ name="payments_delete_invoice_line_item",
923
+ response_model=InvoiceOut,
924
+ dependencies=[Depends(require_idempotency_key)],
925
+ )
926
+ async def delete_invoice_line_item_endpoint(
927
+ provider_invoice_id: str,
928
+ provider_line_item_id: str,
929
+ svc: PaymentsService = Depends(get_service),
930
+ ):
931
+ out = await svc.delete_invoice_line_item(provider_invoice_id, provider_line_item_id)
932
+ await svc.session.flush()
933
+ return out
934
+
935
+ # --- Optional REST alias for detach (calls your existing logic) ---
936
+ @prot.delete(
937
+ "/methods/{provider_method_id}",
938
+ name="payments_delete_method_alias",
939
+ response_model=PaymentMethodOut,
940
+ dependencies=[Depends(require_idempotency_key)],
941
+ )
942
+ async def delete_method_alias(
943
+ provider_method_id: str, svc: PaymentsService = Depends(get_service)
944
+ ):
945
+ out = await svc.detach_payment_method(provider_method_id)
946
+ await svc.session.flush()
947
+ return out
948
+
660
949
  routers.append(svc)
661
950
  routers.append(pub)
662
951
  return routers
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.3
2
2
  Name: svc-infra
3
- Version: 0.1.579
3
+ Version: 0.1.580
4
4
  Summary: Infrastructure for building and deploying prod-ready services
5
5
  License: MIT
6
6
  Keywords: fastapi,sqlalchemy,alembic,auth,infra,async,pydantic
@@ -3,16 +3,16 @@ svc_infra/apf_payments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3
3
3
  svc_infra/apf_payments/alembic.py,sha256=XJIcCDOoaO1EeJbSx_qK9o4cBi430qyo5gECtjHIojw,299
4
4
  svc_infra/apf_payments/models.py,sha256=u4U5oszha5uulCIrNoajaFDIc5YmTlh2mtm-yJUvr9I,14251
5
5
  svc_infra/apf_payments/provider/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
6
- svc_infra/apf_payments/provider/base.py,sha256=sibrBvfULNPCPldokj2f_kDjxJo5zP06YHB1z5i4s3E,5016
6
+ svc_infra/apf_payments/provider/base.py,sha256=1t5znglpGFhjt4zdbuzE5VlHvGarFwzH2oscK8yyKNY,7678
7
7
  svc_infra/apf_payments/provider/registry.py,sha256=NZ4pUkFcbXNtqWEpFeI3NwoKRYGWe9fVQakmlrVLTKE,878
8
8
  svc_infra/apf_payments/provider/stripe.py,sha256=rZlQe1BBLBF1aMOqw98aDRZkA7pOgTqkGvXzSG8qE5c,11298
9
- svc_infra/apf_payments/schemas.py,sha256=BKzaKQaU4iEdT3onpP0dPfh33nU3AieHZLs_79cMp4M,6747
10
- svc_infra/apf_payments/service.py,sha256=325Y118HWULmXX62S1wSrA8Guabtoa7Um0lzvMPDQZM,23872
9
+ svc_infra/apf_payments/schemas.py,sha256=XfBxx6z_Y6cdHktanafNDhhgWl8JVvag9vp2ORmvn_4,8403
10
+ svc_infra/apf_payments/service.py,sha256=bn3BTOTdfkJ4b0Z9cHuFHvlMcv9B1b2n0v-unveUplA,31060
11
11
  svc_infra/apf_payments/settings.py,sha256=VnNQbajbv843buUisqa82xOQ-f5JA8JzHD8o01-yhPQ,1239
12
12
  svc_infra/api/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
13
13
  svc_infra/api/fastapi/__init__.py,sha256=VVdQjak74_wTDqmvL05_C97vIFugQxPVU-3JQEFBgR8,747
14
14
  svc_infra/api/fastapi/apf_payments/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
15
- svc_infra/api/fastapi/apf_payments/router.py,sha256=ff2zYstk1XLSLDwXG0jBViZv624no5OPUSK1YLDJAH4,22464
15
+ svc_infra/api/fastapi/apf_payments/router.py,sha256=zwdAVaUYGT3RjIp-3WtWUMsuBWGPhjt6fAMGt2tZXlg,32456
16
16
  svc_infra/api/fastapi/apf_payments/setup.py,sha256=PX-LHDiyu2eDuaw2m98VPUkF6EmXXRkbjRqh_gL8Kls,2263
17
17
  svc_infra/api/fastapi/auth/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
18
18
  svc_infra/api/fastapi/auth/_cookies.py,sha256=U4heUmMnLezHx8U6ksuUEpSZ6sNMJcIO0gdLpmZ5FXw,1367
@@ -228,7 +228,7 @@ svc_infra/obs/templates/sidecars/railway/__init__.py,sha256=47DEQpj8HBSa-_TImW-5
228
228
  svc_infra/obs/templates/sidecars/railway/agent.yaml,sha256=hYv35yG92XEP_4joMFmMcVTD-4fG_zHitmChjreUJh4,516
229
229
  svc_infra/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
230
230
  svc_infra/utils.py,sha256=VX1yjTx61-YvAymyRhGy18DhybiVdPddiYD_FlKTbJU,952
231
- svc_infra-0.1.579.dist-info/METADATA,sha256=j_kXai0eMsASVgOLCR3hYP0Op-b_Pk3bu5QR_7DMfmY,3487
232
- svc_infra-0.1.579.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
233
- svc_infra-0.1.579.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
234
- svc_infra-0.1.579.dist-info/RECORD,,
231
+ svc_infra-0.1.580.dist-info/METADATA,sha256=IHm9XHfQc8WdbmNpAyFDv0H1fLOIX2LMX0D7KRsmXe0,3487
232
+ svc_infra-0.1.580.dist-info/WHEEL,sha256=IYZQI976HJqqOpQU6PHkJ8fb3tMNBFjg-Cn-pwAbaFM,88
233
+ svc_infra-0.1.580.dist-info/entry_points.txt,sha256=6x_nZOsjvn6hRZsMgZLgTasaCSKCgAjsGhACe_CiP0U,48
234
+ svc_infra-0.1.580.dist-info/RECORD,,