agentref 1.0.4__tar.gz → 2.0.0__tar.gz

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.
Files changed (28) hide show
  1. {agentref-1.0.4 → agentref-2.0.0}/PKG-INFO +1 -1
  2. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/merchant.py +12 -0
  3. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/payout_info.py +8 -0
  4. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/programs.py +18 -6
  5. {agentref-1.0.4 → agentref-2.0.0}/agentref/types/models.py +60 -19
  6. {agentref-1.0.4 → agentref-2.0.0}/pyproject.toml +1 -1
  7. {agentref-1.0.4 → agentref-2.0.0}/tests/test_async.py +22 -6
  8. {agentref-1.0.4 → agentref-2.0.0}/tests/test_errors.py +53 -3
  9. agentref-2.0.0/tests/test_programs.py +384 -0
  10. agentref-1.0.4/tests/test_programs.py +0 -215
  11. {agentref-1.0.4 → agentref-2.0.0}/.github/workflows/ci.yml +0 -0
  12. {agentref-1.0.4 → agentref-2.0.0}/.github/workflows/publish.yml +0 -0
  13. {agentref-1.0.4 → agentref-2.0.0}/.gitignore +0 -0
  14. {agentref-1.0.4 → agentref-2.0.0}/CHANGELOG.md +0 -0
  15. {agentref-1.0.4 → agentref-2.0.0}/README.md +0 -0
  16. {agentref-1.0.4 → agentref-2.0.0}/agentref/__init__.py +0 -0
  17. {agentref-1.0.4 → agentref-2.0.0}/agentref/_http.py +0 -0
  18. {agentref-1.0.4 → agentref-2.0.0}/agentref/client.py +0 -0
  19. {agentref-1.0.4 → agentref-2.0.0}/agentref/errors.py +0 -0
  20. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/__init__.py +0 -0
  21. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/affiliates.py +0 -0
  22. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/billing.py +0 -0
  23. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/conversions.py +0 -0
  24. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/flags.py +0 -0
  25. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/notifications.py +0 -0
  26. {agentref-1.0.4 → agentref-2.0.0}/agentref/resources/payouts.py +0 -0
  27. {agentref-1.0.4 → agentref-2.0.0}/agentref/types/__init__.py +0 -0
  28. {agentref-1.0.4 → agentref-2.0.0}/tests/test_http.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: agentref
3
- Version: 1.0.4
3
+ Version: 2.0.0
4
4
  Summary: Official Python SDK for the AgentRef Affiliate API
5
5
  Author-email: AgentRef <hi@agentref.dev>
6
6
  License: MIT
@@ -23,6 +23,9 @@ class MerchantResource:
23
23
  timezone: Optional[str] = None,
24
24
  default_cookie_duration: Optional[int] = None,
25
25
  default_payout_threshold: Optional[int] = None,
26
+ tracking_requires_consent: Optional[bool] = None,
27
+ tracking_param_aliases: Optional[list[str]] = None,
28
+ tracking_legacy_metadata_fallback_enabled: Optional[bool] = None,
26
29
  ) -> Merchant:
27
30
  payload = UpdateMerchantParams(
28
31
  company_name=company_name,
@@ -31,6 +34,9 @@ class MerchantResource:
31
34
  timezone=timezone,
32
35
  default_cookie_duration=default_cookie_duration,
33
36
  default_payout_threshold=default_payout_threshold,
37
+ tracking_requires_consent=tracking_requires_consent,
38
+ tracking_param_aliases=tracking_param_aliases,
39
+ tracking_legacy_metadata_fallback_enabled=tracking_legacy_metadata_fallback_enabled,
34
40
  ).model_dump(by_alias=True, exclude_none=True)
35
41
  envelope = self._http.request("PATCH", "/merchant", json=payload)
36
42
  return Merchant.model_validate(envelope["data"])
@@ -61,6 +67,9 @@ class AsyncMerchantResource:
61
67
  timezone: Optional[str] = None,
62
68
  default_cookie_duration: Optional[int] = None,
63
69
  default_payout_threshold: Optional[int] = None,
70
+ tracking_requires_consent: Optional[bool] = None,
71
+ tracking_param_aliases: Optional[list[str]] = None,
72
+ tracking_legacy_metadata_fallback_enabled: Optional[bool] = None,
64
73
  ) -> Merchant:
65
74
  payload = UpdateMerchantParams(
66
75
  company_name=company_name,
@@ -69,6 +78,9 @@ class AsyncMerchantResource:
69
78
  timezone=timezone,
70
79
  default_cookie_duration=default_cookie_duration,
71
80
  default_payout_threshold=default_payout_threshold,
81
+ tracking_requires_consent=tracking_requires_consent,
82
+ tracking_param_aliases=tracking_param_aliases,
83
+ tracking_legacy_metadata_fallback_enabled=tracking_legacy_metadata_fallback_enabled,
72
84
  ).model_dump(by_alias=True, exclude_none=True)
73
85
  envelope = await self._http.request("PATCH", "/merchant", json=payload)
74
86
  return Merchant.model_validate(envelope["data"])
@@ -19,7 +19,9 @@ class PayoutInfoResource:
19
19
  *,
20
20
  payout_method: Optional[str] = None,
21
21
  paypal_email: Optional[str] = None,
22
+ bank_account_holder: Optional[str] = None,
22
23
  bank_iban: Optional[str] = None,
24
+ bank_bic: Optional[str] = None,
23
25
  first_name: Optional[str] = None,
24
26
  last_name: Optional[str] = None,
25
27
  address_line1: Optional[str] = None,
@@ -32,7 +34,9 @@ class PayoutInfoResource:
32
34
  payload = UpdatePayoutInfoParams(
33
35
  payout_method=payout_method,
34
36
  paypal_email=paypal_email,
37
+ bank_account_holder=bank_account_holder,
35
38
  bank_iban=bank_iban,
39
+ bank_bic=bank_bic,
36
40
  first_name=first_name,
37
41
  last_name=last_name,
38
42
  address_line1=address_line1,
@@ -59,7 +63,9 @@ class AsyncPayoutInfoResource:
59
63
  *,
60
64
  payout_method: Optional[str] = None,
61
65
  paypal_email: Optional[str] = None,
66
+ bank_account_holder: Optional[str] = None,
62
67
  bank_iban: Optional[str] = None,
68
+ bank_bic: Optional[str] = None,
63
69
  first_name: Optional[str] = None,
64
70
  last_name: Optional[str] = None,
65
71
  address_line1: Optional[str] = None,
@@ -72,7 +78,9 @@ class AsyncPayoutInfoResource:
72
78
  payload = UpdatePayoutInfoParams(
73
79
  payout_method=payout_method,
74
80
  paypal_email=paypal_email,
81
+ bank_account_holder=bank_account_holder,
75
82
  bank_iban=bank_iban,
83
+ bank_bic=bank_bic,
76
84
  first_name=first_name,
77
85
  last_name=last_name,
78
86
  address_line1=address_line1,
@@ -68,6 +68,8 @@ class ProgramsResource:
68
68
  description: Optional[str] = None,
69
69
  landing_page_url: Optional[str] = None,
70
70
  commission_limit_months: Optional[int] = None,
71
+ portal_slug: Optional[str] = None,
72
+ currency: Optional[str] = None,
71
73
  idempotency_key: Optional[str] = None,
72
74
  ) -> Program:
73
75
  body: Dict[str, Any] = {
@@ -80,6 +82,8 @@ class ProgramsResource:
80
82
  "description": description,
81
83
  "landingPageUrl": landing_page_url,
82
84
  "commissionLimitMonths": commission_limit_months,
85
+ "portalSlug": portal_slug,
86
+ "currency": currency,
83
87
  }
84
88
  envelope = self._http.request(
85
89
  "POST",
@@ -102,8 +106,9 @@ class ProgramsResource:
102
106
  description: Optional[str] = None,
103
107
  landing_page_url: Optional[str] = None,
104
108
  status: Optional[Literal["active", "paused", "archived"]] = None,
105
- is_public: Optional[bool] = None,
106
109
  commission_limit_months: Optional[int] = None,
110
+ portal_slug: Optional[str] = None,
111
+ currency: Optional[str] = None,
107
112
  ) -> Program:
108
113
  body: Dict[str, Any] = {
109
114
  "name": name,
@@ -115,8 +120,9 @@ class ProgramsResource:
115
120
  "description": description,
116
121
  "landingPageUrl": landing_page_url,
117
122
  "status": status,
118
- "isPublic": is_public,
119
123
  "commissionLimitMonths": commission_limit_months,
124
+ "portalSlug": portal_slug,
125
+ "currency": currency,
120
126
  }
121
127
  envelope = self._http.request("PATCH", f"/programs/{id}", json={k: v for k, v in body.items() if v is not None})
122
128
  return Program.model_validate(envelope["data"])
@@ -227,7 +233,7 @@ class ProgramsResource:
227
233
  self,
228
234
  id: str,
229
235
  *,
230
- status: Optional[Literal["private", "public"]] = None,
236
+ status: Optional[Literal["private", "pending", "public"]] = None,
231
237
  category: Optional[str] = None,
232
238
  description: Optional[str] = None,
233
239
  logo_url: Optional[str] = None,
@@ -297,6 +303,8 @@ class AsyncProgramsResource:
297
303
  description: Optional[str] = None,
298
304
  landing_page_url: Optional[str] = None,
299
305
  commission_limit_months: Optional[int] = None,
306
+ portal_slug: Optional[str] = None,
307
+ currency: Optional[str] = None,
300
308
  idempotency_key: Optional[str] = None,
301
309
  ) -> Program:
302
310
  body: Dict[str, Any] = {
@@ -309,6 +317,8 @@ class AsyncProgramsResource:
309
317
  "description": description,
310
318
  "landingPageUrl": landing_page_url,
311
319
  "commissionLimitMonths": commission_limit_months,
320
+ "portalSlug": portal_slug,
321
+ "currency": currency,
312
322
  }
313
323
  envelope = await self._http.request(
314
324
  "POST",
@@ -331,8 +341,9 @@ class AsyncProgramsResource:
331
341
  description: Optional[str] = None,
332
342
  landing_page_url: Optional[str] = None,
333
343
  status: Optional[Literal["active", "paused", "archived"]] = None,
334
- is_public: Optional[bool] = None,
335
344
  commission_limit_months: Optional[int] = None,
345
+ portal_slug: Optional[str] = None,
346
+ currency: Optional[str] = None,
336
347
  ) -> Program:
337
348
  body: Dict[str, Any] = {
338
349
  "name": name,
@@ -344,8 +355,9 @@ class AsyncProgramsResource:
344
355
  "description": description,
345
356
  "landingPageUrl": landing_page_url,
346
357
  "status": status,
347
- "isPublic": is_public,
348
358
  "commissionLimitMonths": commission_limit_months,
359
+ "portalSlug": portal_slug,
360
+ "currency": currency,
349
361
  }
350
362
  envelope = await self._http.request("PATCH", f"/programs/{id}", json={k: v for k, v in body.items() if v is not None})
351
363
  return Program.model_validate(envelope["data"])
@@ -456,7 +468,7 @@ class AsyncProgramsResource:
456
468
  self,
457
469
  id: str,
458
470
  *,
459
- status: Optional[Literal["private", "public"]] = None,
471
+ status: Optional[Literal["private", "pending", "public"]] = None,
460
472
  category: Optional[str] = None,
461
473
  description: Optional[str] = None,
462
474
  logo_url: Optional[str] = None,
@@ -32,18 +32,26 @@ class Program(BaseModel):
32
32
  model_config = _API_CONFIG
33
33
 
34
34
  id: str
35
+ merchant_id: str
35
36
  name: str
37
+ description: Optional[str] = None
38
+ slug: str
39
+ landing_page_url: Optional[str] = None
40
+ portal_slug: Optional[str] = None
41
+ status: str
42
+ marketplace_status: str
43
+ marketplace_category: Optional[str] = None
44
+ marketplace_description: Optional[str] = None
45
+ marketplace_logo_url: Optional[str] = None
36
46
  commission_type: str
37
47
  commission_percent: float
38
48
  commission_limit_months: Optional[int] = None
49
+ commission_hold_days: int
39
50
  cookie_duration: int
40
51
  payout_threshold: int
52
+ currency: str
41
53
  auto_approve_affiliates: bool
42
- description: Optional[str] = None
43
- landing_page_url: Optional[str] = None
44
- status: str
45
- is_public: bool
46
- merchant_id: str
54
+ terms_url: Optional[str] = None
47
55
  created_at: str
48
56
  updated_at: str
49
57
 
@@ -51,17 +59,21 @@ class Program(BaseModel):
51
59
  class ProgramStats(BaseModel):
52
60
  model_config = _API_CONFIG
53
61
 
54
- clicks: int
55
- conversions: int
56
- revenue: float
57
- commissions: float
58
- period: str
62
+ program_id: str
63
+ program_name: str
64
+ status: str
65
+ total_revenue: float
66
+ total_conversions: int
67
+ total_commissions: float
68
+ pending_commissions: float
69
+ active_affiliates: int
70
+ conversions_by_status: Dict[str, int]
59
71
 
60
72
 
61
73
  class UpdateProgramMarketplaceParams(BaseModel):
62
74
  model_config = _API_CONFIG
63
75
 
64
- status: Optional[Literal["private", "public"]] = None
76
+ status: Optional[Literal["private", "pending", "public"]] = None
65
77
  category: Optional[str] = None
66
78
  description: Optional[str] = None
67
79
  logo_url: Optional[str] = None
@@ -209,13 +221,32 @@ class Merchant(BaseModel):
209
221
  model_config = _API_CONFIG
210
222
 
211
223
  id: str
212
- email: str
213
- company_name: Optional[str] = None
214
- domain: Optional[str] = None
215
- domain_verified: bool
216
- trust_level: str
217
- stripe_connected: bool
224
+ user_id: str
225
+ company_name: str
226
+ website: Optional[str] = None
227
+ logo_url: Optional[str] = None
228
+ stripe_account_id: Optional[str] = None
229
+ stripe_connected_at: Optional[str] = None
230
+ billing_tier: str
231
+ stripe_customer_id: Optional[str] = None
232
+ stripe_subscription_id: Optional[str] = None
233
+ payment_status: str
234
+ last_payment_failed_at: Optional[str] = None
235
+ default_cookie_duration: int
236
+ default_payout_threshold: int
237
+ timezone: str
238
+ tracking_requires_consent: bool
239
+ tracking_param_aliases: List[str]
240
+ tracking_legacy_metadata_fallback_enabled: bool
241
+ state: str
242
+ verified_domain: Optional[str] = None
243
+ domain_verification_token: Optional[str] = None
244
+ domain_verified_at: Optional[str] = None
245
+ notification_preferences: Optional[Dict[str, bool]] = None
246
+ onboarding_completed: bool
247
+ onboarding_step: int
218
248
  created_at: str
249
+ updated_at: str
219
250
 
220
251
 
221
252
  class UpdateMerchantParams(BaseModel):
@@ -227,14 +258,20 @@ class UpdateMerchantParams(BaseModel):
227
258
  timezone: Optional[str] = None
228
259
  default_cookie_duration: Optional[int] = None
229
260
  default_payout_threshold: Optional[int] = None
261
+ tracking_requires_consent: Optional[bool] = None
262
+ tracking_param_aliases: Optional[List[str]] = None
263
+ tracking_legacy_metadata_fallback_enabled: Optional[bool] = None
230
264
 
231
265
 
232
266
  class MerchantDomainStatus(BaseModel):
233
267
  model_config = _API_CONFIG
234
268
 
269
+ status: str
235
270
  domain: Optional[str] = None
236
- verified: bool
237
271
  txt_record: Optional[str] = None
272
+ verified_at: Optional[str] = None
273
+ tracking_mode: str
274
+ advanced_tracking_enabled: bool
238
275
 
239
276
 
240
277
  class StripeConnectSession(BaseModel):
@@ -268,7 +305,9 @@ class PayoutInfo(BaseModel):
268
305
 
269
306
  payout_method: Optional[str] = None
270
307
  paypal_email: Optional[str] = None
308
+ bank_account_holder: Optional[str] = None
271
309
  bank_iban: Optional[str] = None
310
+ bank_bic: Optional[str] = None
272
311
  first_name: Optional[str] = None
273
312
  last_name: Optional[str] = None
274
313
  address_line1: Optional[str] = None
@@ -284,7 +323,9 @@ class UpdatePayoutInfoParams(BaseModel):
284
323
 
285
324
  payout_method: Optional[str] = None
286
325
  paypal_email: Optional[str] = None
326
+ bank_account_holder: Optional[str] = None
287
327
  bank_iban: Optional[str] = None
328
+ bank_bic: Optional[str] = None
288
329
  first_name: Optional[str] = None
289
330
  last_name: Optional[str] = None
290
331
  address_line1: Optional[str] = None
@@ -302,7 +343,7 @@ class NotificationPreferences(BaseModel):
302
343
  new_conversion: bool = True
303
344
  commission_approved: bool = True
304
345
  payout_processed: bool = True
305
- weekly_digest: bool = True
346
+ weekly_digest: bool = False
306
347
 
307
348
 
308
349
  class UpdateNotificationPreferencesParams(BaseModel):
@@ -4,7 +4,7 @@ build-backend = "hatchling.build"
4
4
 
5
5
  [project]
6
6
  name = "agentref"
7
- version = "1.0.4"
7
+ version = "2.0.0"
8
8
  description = "Official Python SDK for the AgentRef Affiliate API"
9
9
  authors = [{ name = "AgentRef", email = "hi@agentref.dev" }]
10
10
  license = { text = "MIT" }
@@ -40,18 +40,26 @@ async def test_async_list_all_stops_on_has_more_false() -> None:
40
40
  "data": [
41
41
  {
42
42
  "id": "prog_1",
43
+ "merchantId": "merch_1",
43
44
  "name": "Program",
44
45
  "description": None,
46
+ "slug": "program",
45
47
  "landingPageUrl": None,
48
+ "portalSlug": "program",
49
+ "status": "active",
50
+ "marketplaceStatus": "public",
51
+ "marketplaceCategory": None,
52
+ "marketplaceDescription": None,
53
+ "marketplaceLogoUrl": None,
46
54
  "commissionType": "one_time",
47
55
  "commissionPercent": 20,
48
56
  "commissionLimitMonths": None,
57
+ "commissionHoldDays": 30,
49
58
  "cookieDuration": 30,
50
59
  "payoutThreshold": 5000,
60
+ "currency": "USD",
51
61
  "autoApproveAffiliates": True,
52
- "status": "active",
53
- "isPublic": True,
54
- "merchantId": "merch_1",
62
+ "termsUrl": None,
55
63
  "createdAt": "2026-01-01T00:00:00Z",
56
64
  "updatedAt": "2026-01-01T00:00:00Z",
57
65
  }
@@ -71,18 +79,26 @@ async def test_async_list_all_stops_on_has_more_false() -> None:
71
79
  "data": [
72
80
  {
73
81
  "id": "prog_2",
82
+ "merchantId": "merch_1",
74
83
  "name": "Program",
75
84
  "description": None,
85
+ "slug": "program",
76
86
  "landingPageUrl": None,
87
+ "portalSlug": "program",
88
+ "status": "active",
89
+ "marketplaceStatus": "public",
90
+ "marketplaceCategory": None,
91
+ "marketplaceDescription": None,
92
+ "marketplaceLogoUrl": None,
77
93
  "commissionType": "one_time",
78
94
  "commissionPercent": 20,
79
95
  "commissionLimitMonths": None,
96
+ "commissionHoldDays": 30,
80
97
  "cookieDuration": 30,
81
98
  "payoutThreshold": 5000,
99
+ "currency": "USD",
82
100
  "autoApproveAffiliates": True,
83
- "status": "active",
84
- "isPublic": True,
85
- "merchantId": "merch_1",
101
+ "termsUrl": None,
86
102
  "createdAt": "2026-01-01T00:00:00Z",
87
103
  "updatedAt": "2026-01-01T00:00:00Z",
88
104
  }
@@ -38,15 +38,26 @@ def test_pydantic_models_parse_camel_case() -> None:
38
38
  program = Program.model_validate(
39
39
  {
40
40
  "id": "p1",
41
+ "merchantId": "m1",
41
42
  "name": "Test",
43
+ "description": None,
44
+ "slug": "test",
45
+ "landingPageUrl": None,
46
+ "portalSlug": "test",
47
+ "status": "active",
48
+ "marketplaceStatus": "public",
49
+ "marketplaceCategory": None,
50
+ "marketplaceDescription": None,
51
+ "marketplaceLogoUrl": None,
42
52
  "commissionType": "one_time",
43
53
  "commissionPercent": 20.0,
54
+ "commissionLimitMonths": None,
55
+ "commissionHoldDays": 30,
44
56
  "cookieDuration": 30,
45
57
  "payoutThreshold": 5000,
58
+ "currency": "USD",
46
59
  "autoApproveAffiliates": True,
47
- "status": "active",
48
- "isPublic": True,
49
- "merchantId": "m1",
60
+ "termsUrl": None,
50
61
  "createdAt": "2026-01-01T00:00:00Z",
51
62
  "updatedAt": "2026-01-01T00:00:00Z",
52
63
  }
@@ -58,6 +69,45 @@ def test_pydantic_models_parse_camel_case() -> None:
58
69
  assert program.auto_approve_affiliates is True
59
70
 
60
71
 
72
+ def test_merchant_model_parses_current_rest_shape() -> None:
73
+ from agentref.types.models import Merchant
74
+
75
+ merchant = Merchant.model_validate(
76
+ {
77
+ "id": "merch_1",
78
+ "userId": "user_1",
79
+ "companyName": "AgentRef Inc",
80
+ "website": "https://agentref.dev",
81
+ "logoUrl": None,
82
+ "stripeAccountId": None,
83
+ "stripeConnectedAt": None,
84
+ "billingTier": "free",
85
+ "stripeCustomerId": None,
86
+ "stripeSubscriptionId": None,
87
+ "paymentStatus": "active",
88
+ "lastPaymentFailedAt": None,
89
+ "defaultCookieDuration": 30,
90
+ "defaultPayoutThreshold": 5000,
91
+ "timezone": "UTC",
92
+ "trackingRequiresConsent": True,
93
+ "trackingParamAliases": ["ref"],
94
+ "trackingLegacyMetadataFallbackEnabled": True,
95
+ "state": "verified",
96
+ "verifiedDomain": "agentref.dev",
97
+ "domainVerificationToken": None,
98
+ "domainVerifiedAt": "2026-01-01T00:00:00Z",
99
+ "notificationPreferences": {"newAffiliate": True},
100
+ "onboardingCompleted": True,
101
+ "onboardingStep": 4,
102
+ "createdAt": "2026-01-01T00:00:00Z",
103
+ "updatedAt": "2026-01-02T00:00:00Z",
104
+ }
105
+ )
106
+
107
+ assert merchant.state == "verified"
108
+ assert merchant.verified_domain == "agentref.dev"
109
+
110
+
61
111
  def test_pagination_meta_parses_camel_case() -> None:
62
112
  from agentref.types.models import PaginationMeta
63
113
 
@@ -0,0 +1,384 @@
1
+ from __future__ import annotations
2
+
3
+ import json
4
+
5
+ import httpx
6
+ import respx
7
+
8
+ from agentref import AgentRef
9
+
10
+
11
+ def _mock_program() -> dict:
12
+ return {
13
+ "id": "prog_1",
14
+ "merchantId": "merch_1",
15
+ "name": "Program",
16
+ "description": None,
17
+ "slug": "program",
18
+ "landingPageUrl": None,
19
+ "portalSlug": "program",
20
+ "status": "active",
21
+ "marketplaceStatus": "public",
22
+ "marketplaceCategory": None,
23
+ "marketplaceDescription": None,
24
+ "marketplaceLogoUrl": None,
25
+ "commissionType": "one_time",
26
+ "commissionPercent": 20,
27
+ "commissionLimitMonths": None,
28
+ "commissionHoldDays": 30,
29
+ "cookieDuration": 30,
30
+ "payoutThreshold": 5000,
31
+ "currency": "USD",
32
+ "autoApproveAffiliates": True,
33
+ "termsUrl": None,
34
+ "createdAt": "2026-01-01T00:00:00Z",
35
+ "updatedAt": "2026-01-01T00:00:00Z",
36
+ }
37
+
38
+
39
+ def test_create_uses_real_field_names() -> None:
40
+ client = AgentRef(api_key="ak_live_test")
41
+
42
+ with respx.mock:
43
+ route = respx.post("https://www.agentref.dev/api/v1/programs").mock(
44
+ return_value=httpx.Response(201, json={"data": _mock_program(), "meta": {"requestId": "r"}})
45
+ )
46
+
47
+ client.programs.create(
48
+ name="Test",
49
+ commission_type="one_time",
50
+ commission_percent=20,
51
+ cookie_duration=30,
52
+ portal_slug="test-program",
53
+ currency="EUR",
54
+ )
55
+
56
+ body = json.loads(route.calls[0].request.content)
57
+
58
+ assert "commissionType" in body
59
+ assert "commissionPercent" in body
60
+ assert "cookieDuration" in body
61
+ assert body["portalSlug"] == "test-program"
62
+ assert body["currency"] == "EUR"
63
+ assert "commissionRate" not in body
64
+ assert "cookieDays" not in body
65
+
66
+
67
+ def test_list_all_stops_on_has_more_false() -> None:
68
+ client = AgentRef(api_key="ak_live_test")
69
+
70
+ with respx.mock:
71
+ respx.get("https://www.agentref.dev/api/v1/programs").mock(
72
+ side_effect=[
73
+ httpx.Response(
74
+ 200,
75
+ json={
76
+ "data": [_mock_program()],
77
+ "meta": {"total": 2, "page": 1, "pageSize": 1, "hasMore": True, "requestId": "r1"},
78
+ },
79
+ ),
80
+ httpx.Response(
81
+ 200,
82
+ json={
83
+ "data": [{**_mock_program(), "id": "prog_2"}],
84
+ "meta": {"total": 2, "page": 2, "pageSize": 1, "hasMore": False, "requestId": "r2"},
85
+ },
86
+ ),
87
+ ]
88
+ )
89
+
90
+ all_programs = list(client.programs.list_all(page_size=1))
91
+
92
+ assert len(all_programs) == 2
93
+ assert all_programs[0].id == "prog_1"
94
+ assert all_programs[1].id == "prog_2"
95
+
96
+
97
+ def test_flags_resolve_sends_block_affiliate_true() -> None:
98
+ client = AgentRef(api_key="ak_live_test")
99
+
100
+ with respx.mock:
101
+ route = respx.post("https://www.agentref.dev/api/v1/flags/flag_1/resolve").mock(
102
+ return_value=httpx.Response(
103
+ 200,
104
+ json={
105
+ "data": {
106
+ "id": "flag_1",
107
+ "affiliateId": "aff_1",
108
+ "type": "manual_review",
109
+ "status": "confirmed",
110
+ "details": {"source": "test"},
111
+ "note": "confirmed fraud",
112
+ "createdAt": "2026-01-01T00:00:00Z",
113
+ "resolvedAt": "2026-01-01T01:00:00Z",
114
+ },
115
+ "meta": {"requestId": "r"},
116
+ },
117
+ )
118
+ )
119
+
120
+ client.flags.resolve(
121
+ "flag_1",
122
+ status="confirmed",
123
+ note="confirmed fraud",
124
+ block_affiliate=True,
125
+ idempotency_key="idem-flag-1",
126
+ )
127
+
128
+ body = json.loads(route.calls[0].request.content)
129
+
130
+ assert body["status"] == "confirmed"
131
+ assert body["blockAffiliate"] is True
132
+
133
+
134
+ def test_list_invites_returns_typed_invites() -> None:
135
+ client = AgentRef(api_key="ak_live_test")
136
+
137
+ with respx.mock:
138
+ respx.get("https://www.agentref.dev/api/v1/programs/prog_1/invites").mock(
139
+ return_value=httpx.Response(
140
+ 200,
141
+ json={
142
+ "data": [
143
+ {
144
+ "token": "tok_1",
145
+ "email": "affiliate@example.com",
146
+ "programId": "prog_1",
147
+ "expiresAt": "2026-12-01T00:00:00Z",
148
+ "createdAt": "2026-01-01T00:00:00Z",
149
+ }
150
+ ],
151
+ "meta": {"requestId": "r"},
152
+ },
153
+ )
154
+ )
155
+ invites = client.programs.list_invites("prog_1")
156
+ assert invites[0].token == "tok_1"
157
+
158
+
159
+ def test_update_marketplace_uses_camel_case_payload() -> None:
160
+ client = AgentRef(api_key="ak_live_test")
161
+
162
+ with respx.mock:
163
+ route = respx.patch("https://www.agentref.dev/api/v1/programs/prog_1/marketplace").mock(
164
+ return_value=httpx.Response(200, json={"data": {"status": "pending"}, "meta": {"requestId": "r"}})
165
+ )
166
+ client.programs.update_marketplace("prog_1", status="pending", logo_url="https://cdn.example.com/logo.png")
167
+ body = json.loads(route.calls[0].request.content)
168
+ assert body["status"] == "pending"
169
+ assert body["logoUrl"] == "https://cdn.example.com/logo.png"
170
+
171
+
172
+ def test_payouts_create_sends_idempotency_and_body() -> None:
173
+ client = AgentRef(api_key="ak_live_test")
174
+
175
+ with respx.mock:
176
+ route = respx.post("https://www.agentref.dev/api/v1/payouts").mock(
177
+ return_value=httpx.Response(201, json={"data": {"id": "pay_1"}, "meta": {"requestId": "r"}})
178
+ )
179
+ client.payouts.create(
180
+ affiliate_id="aff_1",
181
+ program_id="prog_1",
182
+ method="paypal",
183
+ idempotency_key="idem-payout-1",
184
+ )
185
+ body = json.loads(route.calls[0].request.content)
186
+ idempotency = route.calls[0].request.headers.get("idempotency-key")
187
+ assert body["affiliateId"] == "aff_1"
188
+ assert body["programId"] == "prog_1"
189
+ assert body["method"] == "paypal"
190
+ assert idempotency == "idem-payout-1"
191
+
192
+
193
+ def test_merchant_update_and_connect_stripe() -> None:
194
+ client = AgentRef(api_key="ak_live_test")
195
+
196
+ with respx.mock:
197
+ update_route = respx.patch("https://www.agentref.dev/api/v1/merchant").mock(
198
+ return_value=httpx.Response(
199
+ 200,
200
+ json={
201
+ "data": {
202
+ "id": "merch_1",
203
+ "userId": "user_1",
204
+ "companyName": "AgentRef Inc",
205
+ "website": "https://agentref.dev",
206
+ "logoUrl": None,
207
+ "stripeAccountId": None,
208
+ "stripeConnectedAt": None,
209
+ "billingTier": "free",
210
+ "stripeCustomerId": None,
211
+ "stripeSubscriptionId": None,
212
+ "paymentStatus": "active",
213
+ "lastPaymentFailedAt": None,
214
+ "defaultCookieDuration": 30,
215
+ "defaultPayoutThreshold": 5000,
216
+ "timezone": "UTC",
217
+ "trackingRequiresConsent": True,
218
+ "trackingParamAliases": ["ref", "partner"],
219
+ "trackingLegacyMetadataFallbackEnabled": True,
220
+ "state": "verified",
221
+ "verifiedDomain": "agentref.dev",
222
+ "domainVerificationToken": None,
223
+ "domainVerifiedAt": "2026-01-01T00:00:00Z",
224
+ "notificationPreferences": {"newAffiliate": True},
225
+ "onboardingCompleted": True,
226
+ "onboardingStep": 4,
227
+ "createdAt": "2026-01-01T00:00:00Z",
228
+ "updatedAt": "2026-01-02T00:00:00Z",
229
+ },
230
+ "meta": {"requestId": "r"},
231
+ },
232
+ )
233
+ )
234
+ connect_route = respx.post("https://www.agentref.dev/api/v1/merchant/connect-stripe").mock(
235
+ return_value=httpx.Response(200, json={"data": {"url": "https://connect.stripe.com/x"}, "meta": {"requestId": "r"}})
236
+ )
237
+
238
+ merchant = client.merchant.update(
239
+ company_name="AgentRef Inc",
240
+ tracking_requires_consent=True,
241
+ tracking_param_aliases=["ref", "partner"],
242
+ )
243
+ connect = client.merchant.connect_stripe()
244
+ update_body = json.loads(update_route.calls[0].request.content)
245
+ connect_method = connect_route.calls[0].request.method
246
+
247
+ assert merchant.company_name == "AgentRef Inc"
248
+ assert update_body["companyName"] == "AgentRef Inc"
249
+ assert update_body["trackingRequiresConsent"] is True
250
+ assert update_body["trackingParamAliases"] == ["ref", "partner"]
251
+ assert merchant.state == "verified"
252
+ assert merchant.verified_domain == "agentref.dev"
253
+ assert connect.url.startswith("https://connect.stripe.com")
254
+ assert connect_method == "POST"
255
+
256
+
257
+ def test_merchant_domain_status_uses_current_contract() -> None:
258
+ client = AgentRef(api_key="ak_live_test")
259
+
260
+ with respx.mock:
261
+ respx.get("https://www.agentref.dev/api/v1/merchant/domain-status").mock(
262
+ return_value=httpx.Response(
263
+ 200,
264
+ json={
265
+ "data": {
266
+ "status": "verified",
267
+ "domain": "agentref.dev",
268
+ "txtRecord": None,
269
+ "verifiedAt": "2026-01-01T00:00:00Z",
270
+ "trackingMode": "advanced",
271
+ "advancedTrackingEnabled": True,
272
+ },
273
+ "meta": {"requestId": "r"},
274
+ },
275
+ )
276
+ )
277
+
278
+ status = client.merchant.domain_status()
279
+
280
+ assert status.status == "verified"
281
+ assert status.tracking_mode == "advanced"
282
+
283
+
284
+ def test_program_stats_uses_current_contract() -> None:
285
+ client = AgentRef(api_key="ak_live_test")
286
+
287
+ with respx.mock:
288
+ respx.get("https://www.agentref.dev/api/v1/programs/prog_1/stats").mock(
289
+ return_value=httpx.Response(
290
+ 200,
291
+ json={
292
+ "data": {
293
+ "programId": "prog_1",
294
+ "programName": "Growth Program",
295
+ "status": "active",
296
+ "totalRevenue": 25000,
297
+ "totalConversions": 12,
298
+ "totalCommissions": 5000,
299
+ "pendingCommissions": 1200,
300
+ "activeAffiliates": 3,
301
+ "conversionsByStatus": {
302
+ "pending": 1,
303
+ "approved": 10,
304
+ "rejected": 1,
305
+ "refunded": 0,
306
+ },
307
+ },
308
+ "meta": {"requestId": "r"},
309
+ },
310
+ )
311
+ )
312
+
313
+ stats = client.programs.stats("prog_1")
314
+
315
+ assert stats.program_id == "prog_1"
316
+ assert stats.conversions_by_status["approved"] == 10
317
+
318
+
319
+ def test_payout_info_supports_bank_fields() -> None:
320
+ client = AgentRef(api_key="ak_live_test")
321
+
322
+ with respx.mock:
323
+ get_route = respx.get("https://www.agentref.dev/api/v1/me/payout-info").mock(
324
+ return_value=httpx.Response(
325
+ 200,
326
+ json={
327
+ "data": {
328
+ "payoutMethod": "bank_transfer",
329
+ "paypalEmail": None,
330
+ "bankAccountHolder": "Jane Doe",
331
+ "bankIban": "****1234",
332
+ "bankBic": "COBADEFFXXX",
333
+ "firstName": "Jane",
334
+ "lastName": "Doe",
335
+ "addressLine1": "Main Street 1",
336
+ "addressLine2": None,
337
+ "city": "Berlin",
338
+ "state": None,
339
+ "postalCode": "10115",
340
+ "vatId": "DE123",
341
+ },
342
+ "meta": {"requestId": "r"},
343
+ },
344
+ )
345
+ )
346
+ update_route = respx.patch("https://www.agentref.dev/api/v1/me/payout-info").mock(
347
+ return_value=httpx.Response(
348
+ 200,
349
+ json={
350
+ "data": {
351
+ "payoutMethod": "bank_transfer",
352
+ "paypalEmail": None,
353
+ "bankAccountHolder": "Jane Doe",
354
+ "bankIban": "****1234",
355
+ "bankBic": "COBADEFFXXX",
356
+ "firstName": "Jane",
357
+ "lastName": "Doe",
358
+ "addressLine1": "Main Street 1",
359
+ "addressLine2": None,
360
+ "city": "Berlin",
361
+ "state": None,
362
+ "postalCode": "10115",
363
+ "vatId": "DE123",
364
+ },
365
+ "meta": {"requestId": "r"},
366
+ },
367
+ )
368
+ )
369
+
370
+ payout_info = client.payout_info.get()
371
+ client.payout_info.update(
372
+ payout_method="bank_transfer",
373
+ bank_account_holder="Jane Doe",
374
+ bank_iban="DE89370400440532013000",
375
+ bank_bic="COBADEFFXXX",
376
+ )
377
+
378
+ update_body = json.loads(update_route.calls[0].request.content)
379
+
380
+ assert get_route.called
381
+ assert payout_info.bank_account_holder == "Jane Doe"
382
+ assert payout_info.bank_bic == "COBADEFFXXX"
383
+ assert update_body["bankAccountHolder"] == "Jane Doe"
384
+ assert update_body["bankBic"] == "COBADEFFXXX"
@@ -1,215 +0,0 @@
1
- from __future__ import annotations
2
-
3
- import json
4
-
5
- import httpx
6
- import respx
7
-
8
- from agentref import AgentRef
9
-
10
-
11
- def _mock_program() -> dict:
12
- return {
13
- "id": "prog_1",
14
- "name": "Program",
15
- "description": None,
16
- "landingPageUrl": None,
17
- "commissionType": "one_time",
18
- "commissionPercent": 20,
19
- "commissionLimitMonths": None,
20
- "cookieDuration": 30,
21
- "payoutThreshold": 5000,
22
- "autoApproveAffiliates": True,
23
- "status": "active",
24
- "isPublic": True,
25
- "merchantId": "merch_1",
26
- "createdAt": "2026-01-01T00:00:00Z",
27
- "updatedAt": "2026-01-01T00:00:00Z",
28
- }
29
-
30
-
31
- def test_create_uses_real_field_names() -> None:
32
- client = AgentRef(api_key="ak_live_test")
33
-
34
- with respx.mock:
35
- route = respx.post("https://www.agentref.dev/api/v1/programs").mock(
36
- return_value=httpx.Response(201, json={"data": _mock_program(), "meta": {"requestId": "r"}})
37
- )
38
-
39
- client.programs.create(
40
- name="Test",
41
- commission_type="one_time",
42
- commission_percent=20,
43
- cookie_duration=30,
44
- )
45
-
46
- body = json.loads(route.calls[0].request.content)
47
-
48
- assert "commissionType" in body
49
- assert "commissionPercent" in body
50
- assert "cookieDuration" in body
51
- assert "commissionRate" not in body
52
- assert "cookieDays" not in body
53
-
54
-
55
- def test_list_all_stops_on_has_more_false() -> None:
56
- client = AgentRef(api_key="ak_live_test")
57
-
58
- with respx.mock:
59
- respx.get("https://www.agentref.dev/api/v1/programs").mock(
60
- side_effect=[
61
- httpx.Response(
62
- 200,
63
- json={
64
- "data": [_mock_program()],
65
- "meta": {"total": 2, "page": 1, "pageSize": 1, "hasMore": True, "requestId": "r1"},
66
- },
67
- ),
68
- httpx.Response(
69
- 200,
70
- json={
71
- "data": [{**_mock_program(), "id": "prog_2"}],
72
- "meta": {"total": 2, "page": 2, "pageSize": 1, "hasMore": False, "requestId": "r2"},
73
- },
74
- ),
75
- ]
76
- )
77
-
78
- all_programs = list(client.programs.list_all(page_size=1))
79
-
80
- assert len(all_programs) == 2
81
- assert all_programs[0].id == "prog_1"
82
- assert all_programs[1].id == "prog_2"
83
-
84
-
85
- def test_flags_resolve_sends_block_affiliate_true() -> None:
86
- client = AgentRef(api_key="ak_live_test")
87
-
88
- with respx.mock:
89
- route = respx.post("https://www.agentref.dev/api/v1/flags/flag_1/resolve").mock(
90
- return_value=httpx.Response(
91
- 200,
92
- json={
93
- "data": {
94
- "id": "flag_1",
95
- "affiliateId": "aff_1",
96
- "type": "manual_review",
97
- "status": "confirmed",
98
- "details": {"source": "test"},
99
- "note": "confirmed fraud",
100
- "createdAt": "2026-01-01T00:00:00Z",
101
- "resolvedAt": "2026-01-01T01:00:00Z",
102
- },
103
- "meta": {"requestId": "r"},
104
- },
105
- )
106
- )
107
-
108
- client.flags.resolve(
109
- "flag_1",
110
- status="confirmed",
111
- note="confirmed fraud",
112
- block_affiliate=True,
113
- idempotency_key="idem-flag-1",
114
- )
115
-
116
- body = json.loads(route.calls[0].request.content)
117
-
118
- assert body["status"] == "confirmed"
119
- assert body["blockAffiliate"] is True
120
-
121
-
122
- def test_list_invites_returns_typed_invites() -> None:
123
- client = AgentRef(api_key="ak_live_test")
124
-
125
- with respx.mock:
126
- respx.get("https://www.agentref.dev/api/v1/programs/prog_1/invites").mock(
127
- return_value=httpx.Response(
128
- 200,
129
- json={
130
- "data": [
131
- {
132
- "token": "tok_1",
133
- "email": "affiliate@example.com",
134
- "programId": "prog_1",
135
- "expiresAt": "2026-12-01T00:00:00Z",
136
- "createdAt": "2026-01-01T00:00:00Z",
137
- }
138
- ],
139
- "meta": {"requestId": "r"},
140
- },
141
- )
142
- )
143
- invites = client.programs.list_invites("prog_1")
144
- assert invites[0].token == "tok_1"
145
-
146
-
147
- def test_update_marketplace_uses_camel_case_payload() -> None:
148
- client = AgentRef(api_key="ak_live_test")
149
-
150
- with respx.mock:
151
- route = respx.patch("https://www.agentref.dev/api/v1/programs/prog_1/marketplace").mock(
152
- return_value=httpx.Response(200, json={"data": {"status": "public"}, "meta": {"requestId": "r"}})
153
- )
154
- client.programs.update_marketplace("prog_1", status="public", logo_url="https://cdn.example.com/logo.png")
155
- body = json.loads(route.calls[0].request.content)
156
- assert body["status"] == "public"
157
- assert body["logoUrl"] == "https://cdn.example.com/logo.png"
158
-
159
-
160
- def test_payouts_create_sends_idempotency_and_body() -> None:
161
- client = AgentRef(api_key="ak_live_test")
162
-
163
- with respx.mock:
164
- route = respx.post("https://www.agentref.dev/api/v1/payouts").mock(
165
- return_value=httpx.Response(201, json={"data": {"id": "pay_1"}, "meta": {"requestId": "r"}})
166
- )
167
- client.payouts.create(
168
- affiliate_id="aff_1",
169
- program_id="prog_1",
170
- method="paypal",
171
- idempotency_key="idem-payout-1",
172
- )
173
- body = json.loads(route.calls[0].request.content)
174
- idempotency = route.calls[0].request.headers.get("idempotency-key")
175
- assert body["affiliateId"] == "aff_1"
176
- assert body["programId"] == "prog_1"
177
- assert body["method"] == "paypal"
178
- assert idempotency == "idem-payout-1"
179
-
180
-
181
- def test_merchant_update_and_connect_stripe() -> None:
182
- client = AgentRef(api_key="ak_live_test")
183
-
184
- with respx.mock:
185
- update_route = respx.patch("https://www.agentref.dev/api/v1/merchant").mock(
186
- return_value=httpx.Response(
187
- 200,
188
- json={
189
- "data": {
190
- "id": "merch_1",
191
- "email": "merchant@example.com",
192
- "companyName": "AgentRef Inc",
193
- "domain": None,
194
- "domainVerified": False,
195
- "trustLevel": "standard",
196
- "stripeConnected": False,
197
- "createdAt": "2026-01-01T00:00:00Z",
198
- },
199
- "meta": {"requestId": "r"},
200
- },
201
- )
202
- )
203
- connect_route = respx.post("https://www.agentref.dev/api/v1/merchant/connect-stripe").mock(
204
- return_value=httpx.Response(200, json={"data": {"url": "https://connect.stripe.com/x"}, "meta": {"requestId": "r"}})
205
- )
206
-
207
- merchant = client.merchant.update(company_name="AgentRef Inc")
208
- connect = client.merchant.connect_stripe()
209
- update_body = json.loads(update_route.calls[0].request.content)
210
- connect_method = connect_route.calls[0].request.method
211
-
212
- assert merchant.company_name == "AgentRef Inc"
213
- assert update_body["companyName"] == "AgentRef Inc"
214
- assert connect.url.startswith("https://connect.stripe.com")
215
- assert connect_method == "POST"
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes
File without changes