provecart 0.1.0__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.
- provecart/__init__.py +21 -0
- provecart/client.py +286 -0
- provecart-0.1.0.dist-info/METADATA +94 -0
- provecart-0.1.0.dist-info/RECORD +6 -0
- provecart-0.1.0.dist-info/WHEEL +5 -0
- provecart-0.1.0.dist-info/top_level.txt +1 -0
provecart/__init__.py
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
"""ProveCart client: verify an agent's cart, booking, or reservation
|
|
2
|
+
against the buyer's signed instructions before payment is captured.
|
|
3
|
+
"""
|
|
4
|
+
from .client import (
|
|
5
|
+
KNOWN_ACTIONS,
|
|
6
|
+
Client,
|
|
7
|
+
ProveCartClient,
|
|
8
|
+
ProveCartError,
|
|
9
|
+
route_verdict,
|
|
10
|
+
)
|
|
11
|
+
|
|
12
|
+
__version__ = "0.1.0"
|
|
13
|
+
|
|
14
|
+
__all__ = [
|
|
15
|
+
"KNOWN_ACTIONS",
|
|
16
|
+
"Client",
|
|
17
|
+
"ProveCartClient",
|
|
18
|
+
"ProveCartError",
|
|
19
|
+
"route_verdict",
|
|
20
|
+
"__version__",
|
|
21
|
+
]
|
provecart/client.py
ADDED
|
@@ -0,0 +1,286 @@
|
|
|
1
|
+
"""ProveCart client + verdict routing.
|
|
2
|
+
|
|
3
|
+
Self-contained (standard library only). Implements the consumer rules
|
|
4
|
+
of the Agent Correction Format (ACF v0.2, com.provecart.conformity):
|
|
5
|
+
|
|
6
|
+
MATCH -> proceed to payment (or ticket issuance).
|
|
7
|
+
BLOCK + fixes -> apply ONLY repairs that bring the transaction
|
|
8
|
+
INTO the buyer's existing authorization, then
|
|
9
|
+
resubmit as a new check carrying
|
|
10
|
+
previous_record_id, within max_attempts.
|
|
11
|
+
REVIEW / unknown -> stop and escalate to the buyer. Never guess,
|
|
12
|
+
action never widen the authorization.
|
|
13
|
+
|
|
14
|
+
API contract (https://api.provecart.com, X-API-Key header):
|
|
15
|
+
POST /mandates -> signed cart mandate (use its "id")
|
|
16
|
+
POST /verify -> cart verdict
|
|
17
|
+
POST /booking-mandates -> signed booking mandate
|
|
18
|
+
POST /verify-booking -> booking verdict
|
|
19
|
+
POST /reservation-mandates -> signed reservation mandate
|
|
20
|
+
POST /verify-reservation -> reservation verdict
|
|
21
|
+
|
|
22
|
+
Every verify response carries {verdict, corrections[], max_attempts,
|
|
23
|
+
record_id, verdict_token, ...}. A corrected resubmission is a new
|
|
24
|
+
verify call against the same mandate with previous_record_id set to
|
|
25
|
+
the earlier check's record_id.
|
|
26
|
+
"""
|
|
27
|
+
import json
|
|
28
|
+
import urllib.error
|
|
29
|
+
import urllib.request
|
|
30
|
+
|
|
31
|
+
KNOWN_ACTIONS = {
|
|
32
|
+
"remove_item", "replace_item", "change_variant", "reduce_quantity",
|
|
33
|
+
"reduce_total", "change_currency", "correct_traveler_name",
|
|
34
|
+
"change_airport", "change_travel_date", "change_departure_time",
|
|
35
|
+
"add_itinerary", "rebuild_itinerary", "change_cabin",
|
|
36
|
+
"change_fare_terms", "add_hotel_stay", "change_room",
|
|
37
|
+
"change_provider", "change_location", "change_date", "change_time",
|
|
38
|
+
"change_party_size", "correct_reservation_name", "reduce_deposit",
|
|
39
|
+
"change_cancellation_terms",
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
DEFAULT_BASE_URL = "https://api.provecart.com"
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
class ProveCartError(RuntimeError):
|
|
46
|
+
"""Raised when the ProveCart API returns a non-200 status."""
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class ProveCartClient:
|
|
50
|
+
"""Client for the ProveCart verification API.
|
|
51
|
+
|
|
52
|
+
Parameters:
|
|
53
|
+
api_key: integrator API key, sent as the X-API-Key header.
|
|
54
|
+
base_url: API base URL (default https://api.provecart.com).
|
|
55
|
+
transport: optional callable ``(path, body) -> (status, dict)``
|
|
56
|
+
replacing the HTTP layer. Intended for offline tests and
|
|
57
|
+
rehearsal runs; when supplied, no network call is made.
|
|
58
|
+
timeout: HTTP timeout in seconds for the default transport.
|
|
59
|
+
"""
|
|
60
|
+
|
|
61
|
+
def __init__(self, api_key, base_url=DEFAULT_BASE_URL,
|
|
62
|
+
transport=None, timeout=20):
|
|
63
|
+
self.api_key = api_key
|
|
64
|
+
self.base = base_url.rstrip("/")
|
|
65
|
+
self._transport = transport
|
|
66
|
+
self.timeout = timeout
|
|
67
|
+
|
|
68
|
+
# -- transport ---------------------------------------------------
|
|
69
|
+
def _post(self, path, body, extra_headers=None):
|
|
70
|
+
if self._transport is not None:
|
|
71
|
+
return self._transport(path, body)
|
|
72
|
+
headers = {"Content-Type": "application/json",
|
|
73
|
+
"X-API-Key": self.api_key}
|
|
74
|
+
if extra_headers:
|
|
75
|
+
headers.update(extra_headers)
|
|
76
|
+
req = urllib.request.Request(
|
|
77
|
+
self.base + path,
|
|
78
|
+
data=json.dumps(body).encode(),
|
|
79
|
+
method="POST",
|
|
80
|
+
headers=headers)
|
|
81
|
+
try:
|
|
82
|
+
with urllib.request.urlopen(req, timeout=self.timeout) as resp:
|
|
83
|
+
return resp.status, json.loads(resp.read())
|
|
84
|
+
except urllib.error.HTTPError as e:
|
|
85
|
+
try:
|
|
86
|
+
return e.code, json.loads(e.read() or b"{}")
|
|
87
|
+
except Exception:
|
|
88
|
+
return e.code, {}
|
|
89
|
+
|
|
90
|
+
def _post_ok(self, path, body, what, idempotency_key=None):
|
|
91
|
+
extra = ({"Idempotency-Key": idempotency_key}
|
|
92
|
+
if idempotency_key else None)
|
|
93
|
+
status, out = self._post(path, body, extra_headers=extra) \
|
|
94
|
+
if extra else self._post(path, body)
|
|
95
|
+
if status != 200:
|
|
96
|
+
raise ProveCartError(f"{what} failed: {status} {out}")
|
|
97
|
+
return out
|
|
98
|
+
|
|
99
|
+
# -- mandate creation ---------------------------------------------
|
|
100
|
+
def create_mandate(self, **fields):
|
|
101
|
+
"""Create a signed cart mandate: POST /mandates.
|
|
102
|
+
|
|
103
|
+
Required fields include user_id, item_description, category,
|
|
104
|
+
max_total_price and expires_at; item_keywords, currency,
|
|
105
|
+
max_quantity, merchant_allowlist/blocklist, brand and
|
|
106
|
+
variant_constraints are also accepted. Returns the signed
|
|
107
|
+
mandate dict; use its "id" as mandate_id.
|
|
108
|
+
"""
|
|
109
|
+
return self._post_ok("/mandates", fields, "mandate creation")
|
|
110
|
+
|
|
111
|
+
# Backwards-compatible alias: the cart mandate is the original kind.
|
|
112
|
+
create_cart_mandate = create_mandate
|
|
113
|
+
|
|
114
|
+
def create_booking_mandate(self, **fields):
|
|
115
|
+
"""Create a signed booking mandate: POST /booking-mandates.
|
|
116
|
+
|
|
117
|
+
Required fields include user_id, travelers, origin,
|
|
118
|
+
destination, departure_date, max_total_price and expires_at.
|
|
119
|
+
Returns the signed mandate dict; use its "id".
|
|
120
|
+
"""
|
|
121
|
+
return self._post_ok("/booking-mandates", fields,
|
|
122
|
+
"booking mandate creation")
|
|
123
|
+
|
|
124
|
+
def create_reservation_mandate(self, **fields):
|
|
125
|
+
"""Create a signed reservation mandate:
|
|
126
|
+
POST /reservation-mandates.
|
|
127
|
+
|
|
128
|
+
Required fields include user_id, service_description, date,
|
|
129
|
+
time, party_size, name_on_reservation, max_total_price and
|
|
130
|
+
expires_at. Returns the signed mandate dict; use its "id".
|
|
131
|
+
"""
|
|
132
|
+
return self._post_ok("/reservation-mandates", fields,
|
|
133
|
+
"reservation mandate creation")
|
|
134
|
+
|
|
135
|
+
# -- verification ---------------------------------------------------
|
|
136
|
+
def verify_cart(self, mandate_id, merchant, items, total_price,
|
|
137
|
+
currency="USD", tax=0.0, shipping=0.0, fees=0.0,
|
|
138
|
+
previous_record_id=None, cart_changed=False,
|
|
139
|
+
idempotency_key=None):
|
|
140
|
+
"""Verify a cart: POST /verify. Returns the verdict dict."""
|
|
141
|
+
body = {"mandate_id": mandate_id, "merchant": merchant,
|
|
142
|
+
"items": items, "total_price": total_price,
|
|
143
|
+
"currency": currency, "tax": tax, "shipping": shipping,
|
|
144
|
+
"fees": fees, "cart_changed": cart_changed}
|
|
145
|
+
if previous_record_id:
|
|
146
|
+
body["previous_record_id"] = previous_record_id
|
|
147
|
+
return self._post_ok("/verify", body, "verify",
|
|
148
|
+
idempotency_key=idempotency_key)
|
|
149
|
+
|
|
150
|
+
def verify_booking(self, mandate_id, merchant, price,
|
|
151
|
+
travelers=None, legs=None, stays=None, fare=None,
|
|
152
|
+
currency="USD", previous_record_id=None,
|
|
153
|
+
booking_changed=False, idempotency_key=None):
|
|
154
|
+
"""Verify a booking: POST /verify-booking.
|
|
155
|
+
|
|
156
|
+
``price`` is a dict with at least {"total": ...}; ``legs``
|
|
157
|
+
items carry from_airport, to_airport, depart_at, arrive_at;
|
|
158
|
+
``travelers`` is a list of traveler names. Returns the
|
|
159
|
+
verdict dict.
|
|
160
|
+
"""
|
|
161
|
+
body = {"mandate_id": mandate_id, "merchant": merchant,
|
|
162
|
+
"price": price, "currency": currency,
|
|
163
|
+
"booking_changed": booking_changed}
|
|
164
|
+
if travelers is not None:
|
|
165
|
+
body["travelers"] = travelers
|
|
166
|
+
if legs is not None:
|
|
167
|
+
body["legs"] = legs
|
|
168
|
+
if stays is not None:
|
|
169
|
+
body["stays"] = stays
|
|
170
|
+
if fare is not None:
|
|
171
|
+
body["fare"] = fare
|
|
172
|
+
if previous_record_id:
|
|
173
|
+
body["previous_record_id"] = previous_record_id
|
|
174
|
+
return self._post_ok("/verify-booking", body, "verify-booking",
|
|
175
|
+
idempotency_key=idempotency_key)
|
|
176
|
+
|
|
177
|
+
def verify_reservation(self, mandate_id, merchant, date, time,
|
|
178
|
+
party_size, name_on_reservation, total_amount,
|
|
179
|
+
provider_name=None, location=None,
|
|
180
|
+
deposit_amount=0.0, currency="USD",
|
|
181
|
+
cancellation=None, previous_record_id=None,
|
|
182
|
+
reservation_changed=False,
|
|
183
|
+
idempotency_key=None):
|
|
184
|
+
"""Verify a reservation: POST /verify-reservation.
|
|
185
|
+
Returns the verdict dict."""
|
|
186
|
+
body = {"mandate_id": mandate_id, "merchant": merchant,
|
|
187
|
+
"date": date, "time": time, "party_size": party_size,
|
|
188
|
+
"name_on_reservation": name_on_reservation,
|
|
189
|
+
"deposit_amount": deposit_amount,
|
|
190
|
+
"total_amount": total_amount, "currency": currency,
|
|
191
|
+
"reservation_changed": reservation_changed}
|
|
192
|
+
if provider_name is not None:
|
|
193
|
+
body["provider_name"] = provider_name
|
|
194
|
+
if location is not None:
|
|
195
|
+
body["location"] = location
|
|
196
|
+
if cancellation is not None:
|
|
197
|
+
body["cancellation"] = cancellation
|
|
198
|
+
if previous_record_id:
|
|
199
|
+
body["previous_record_id"] = previous_record_id
|
|
200
|
+
return self._post_ok("/verify-reservation", body,
|
|
201
|
+
"verify-reservation",
|
|
202
|
+
idempotency_key=idempotency_key)
|
|
203
|
+
|
|
204
|
+
# -- one-call checks: create-or-take a mandate, then verify --------
|
|
205
|
+
def _resolve_mandate_id(self, mandate_id, mandate, creator):
|
|
206
|
+
if mandate is not None:
|
|
207
|
+
created = creator(**mandate)
|
|
208
|
+
return created["id"]
|
|
209
|
+
if not mandate_id:
|
|
210
|
+
raise ValueError(
|
|
211
|
+
"pass mandate_id= or mandate={...} so the check has a "
|
|
212
|
+
"signed instruction to compare against")
|
|
213
|
+
return mandate_id
|
|
214
|
+
|
|
215
|
+
def check_cart(self, merchant, items, total_price, *,
|
|
216
|
+
mandate_id=None, mandate=None, **verify_kwargs):
|
|
217
|
+
"""Check a cart, creating the mandate first when ``mandate``
|
|
218
|
+
(a dict of mandate fields) is given instead of ``mandate_id``.
|
|
219
|
+
Returns the verdict response dict."""
|
|
220
|
+
mid = self._resolve_mandate_id(mandate_id, mandate,
|
|
221
|
+
self.create_mandate)
|
|
222
|
+
return self.verify_cart(mid, merchant, items, total_price,
|
|
223
|
+
**verify_kwargs)
|
|
224
|
+
|
|
225
|
+
def check_booking(self, merchant, price, *, mandate_id=None,
|
|
226
|
+
mandate=None, **verify_kwargs):
|
|
227
|
+
"""Check a booking, creating the booking mandate first when
|
|
228
|
+
``mandate`` is given instead of ``mandate_id``.
|
|
229
|
+
Returns the verdict response dict."""
|
|
230
|
+
mid = self._resolve_mandate_id(mandate_id, mandate,
|
|
231
|
+
self.create_booking_mandate)
|
|
232
|
+
return self.verify_booking(mid, merchant, price, **verify_kwargs)
|
|
233
|
+
|
|
234
|
+
def check_reservation(self, merchant, date, time, party_size,
|
|
235
|
+
name_on_reservation, total_amount, *,
|
|
236
|
+
mandate_id=None, mandate=None, **verify_kwargs):
|
|
237
|
+
"""Check a reservation, creating the reservation mandate
|
|
238
|
+
first when ``mandate`` is given instead of ``mandate_id``.
|
|
239
|
+
Returns the verdict response dict."""
|
|
240
|
+
mid = self._resolve_mandate_id(mandate_id, mandate,
|
|
241
|
+
self.create_reservation_mandate)
|
|
242
|
+
return self.verify_reservation(
|
|
243
|
+
mid, merchant, date, time, party_size, name_on_reservation,
|
|
244
|
+
total_amount, **verify_kwargs)
|
|
245
|
+
|
|
246
|
+
|
|
247
|
+
# Short alias.
|
|
248
|
+
Client = ProveCartClient
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
def route_verdict(response, attempt=1):
|
|
252
|
+
"""Return (decision, detail): decision is 'proceed', 'fix' or
|
|
253
|
+
'escalate'.
|
|
254
|
+
|
|
255
|
+
'fix' is returned only when the verdict is BLOCK, every
|
|
256
|
+
correction is a known action, no correction is marked
|
|
257
|
+
non-correctable, and the retry budget (max_attempts) has not
|
|
258
|
+
been spent. A fix never widens the buyer's authorization: apply
|
|
259
|
+
only the listed corrections, then resubmit carrying the
|
|
260
|
+
response's record_id as previous_record_id. Anything else —
|
|
261
|
+
REVIEW, an unknown verdict, an unknown action — escalates to
|
|
262
|
+
the buyer.
|
|
263
|
+
"""
|
|
264
|
+
verdict = response.get("verdict")
|
|
265
|
+
if verdict == "MATCH":
|
|
266
|
+
return "proceed", {"record_id": response.get("record_id")}
|
|
267
|
+
if verdict == "BLOCK":
|
|
268
|
+
corrections = response.get("corrections") or []
|
|
269
|
+
max_attempts = response.get("max_attempts")
|
|
270
|
+
budget_ok = max_attempts is None or attempt < max_attempts
|
|
271
|
+
usable = (
|
|
272
|
+
bool(corrections)
|
|
273
|
+
and all(c.get("action") in KNOWN_ACTIONS for c in corrections)
|
|
274
|
+
and all(c.get("correctable", True) for c in corrections)
|
|
275
|
+
)
|
|
276
|
+
if usable and budget_ok:
|
|
277
|
+
return "fix", {"corrections": corrections,
|
|
278
|
+
"record_id": response.get("record_id"),
|
|
279
|
+
"max_attempts": max_attempts}
|
|
280
|
+
reason = "BLOCK without usable corrections"
|
|
281
|
+
if usable and not budget_ok:
|
|
282
|
+
reason = "BLOCK retry budget (max_attempts) spent"
|
|
283
|
+
return "escalate", {"reason": reason,
|
|
284
|
+
"record_id": response.get("record_id")}
|
|
285
|
+
return "escalate", {"reason": f"verdict {verdict} needs the buyer",
|
|
286
|
+
"record_id": response.get("record_id")}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: provecart
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: ProveCart client: check an AI agent's cart, booking, or reservation against the buyer's signed instructions before payment is captured.
|
|
5
|
+
License: MIT
|
|
6
|
+
Project-URL: Homepage, https://provecart.com
|
|
7
|
+
Project-URL: Documentation, https://provecart.com/agents
|
|
8
|
+
Project-URL: Correction format, https://provecart.com/standards/correction-format
|
|
9
|
+
Keywords: ai-agents,agent-commerce,verification,payments,provecart
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
13
|
+
Requires-Python: >=3.8
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
|
|
16
|
+
# provecart
|
|
17
|
+
|
|
18
|
+
Your agent put the wrong variant in the cart — the wrong size, the wrong model, a quantity nobody asked for — and your user paid for it. By the time anyone noticed, the order had shipped, and the return came out of somebody's pocket. That is the failure this package exists to stop: one check, between what the agent built and the payment, that compares the transaction against what the buyer actually said.
|
|
19
|
+
|
|
20
|
+
ProveCart is a neutral verification API for agent commerce. You record the buyer's instruction as a signed mandate, send what the agent actually built, and get back a signed verdict — `MATCH`, `REVIEW`, or `BLOCK` — before payment is captured (or, for bookings, before a ticket is issued). A `BLOCK` or `REVIEW` answer carries machine-readable corrections, so the agent can fix what it built and resubmit instead of losing the sale or guessing. Every check is written to a sealed evidence record. ProveCart never holds or moves money.
|
|
21
|
+
|
|
22
|
+
```bash
|
|
23
|
+
pip install provecart
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
Standard library only. No dependencies.
|
|
27
|
+
|
|
28
|
+
## Check a cart
|
|
29
|
+
|
|
30
|
+
```python
|
|
31
|
+
from provecart import ProveCartClient, route_verdict
|
|
32
|
+
client = ProveCartClient(api_key="YOUR_API_KEY")
|
|
33
|
+
verdict = client.check_cart(merchant={"name": "Example Shop", "domain": "shop.example.com"}, items=[{"name": "Wireless Headphones", "variant": "black", "quantity": 1, "unit_price": 249.99}], total_price=249.99, mandate={"user_id": "user-123", "item_description": "Wireless noise cancelling over-ear headphones", "item_keywords": ["wireless", "headphones"], "category": "electronics", "max_total_price": 300.00, "currency": "USD", "max_quantity": 1, "expires_at": "2027-01-01T00:00:00Z"})
|
|
34
|
+
decision, detail = route_verdict(verdict) # proceed / fix / escalate
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
Already holding a mandate id? Pass `mandate_id=` instead of `mandate=` — `check_cart` takes either. The lower-level pair is also there when you want the steps separate: `create_mandate(**fields)` returns the signed mandate, and `verify_cart(mandate_id, merchant, items, total_price, ...)` runs the check.
|
|
38
|
+
|
|
39
|
+
## Check a booking
|
|
40
|
+
|
|
41
|
+
```python
|
|
42
|
+
from provecart import ProveCartClient, route_verdict
|
|
43
|
+
client = ProveCartClient(api_key="YOUR_API_KEY")
|
|
44
|
+
verdict = client.check_booking(merchant={"name": "Example Air", "domain": "air.example.com"}, price={"total": 412.00}, travelers=["Alex Rivera"], legs=[{"from_airport": "SFO", "to_airport": "JFK", "depart_at": "2027-03-10T08:00:00Z", "arrive_at": "2027-03-10T16:30:00Z"}], mandate={"user_id": "user-123", "travelers": [{"name": "Alex Rivera"}], "origin": {"airport": "SFO"}, "destination": {"airport": "JFK"}, "departure_date": "2027-03-10", "max_total_price": 500.00, "currency": "USD", "expires_at": "2027-03-01T00:00:00Z"})
|
|
45
|
+
decision, detail = route_verdict(verdict) # gate both capture and ticket issuance on MATCH
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
The separate steps are `create_booking_mandate(**fields)` and `verify_booking(mandate_id, merchant, price, travelers=..., legs=..., stays=..., fare=...)`.
|
|
49
|
+
|
|
50
|
+
## Check a reservation
|
|
51
|
+
|
|
52
|
+
```python
|
|
53
|
+
from provecart import ProveCartClient, route_verdict
|
|
54
|
+
client = ProveCartClient(api_key="YOUR_API_KEY")
|
|
55
|
+
verdict = client.check_reservation(merchant={"name": "Example Table", "domain": "table.example.com"}, date="2027-03-10", time="19:00", party_size=2, name_on_reservation="Alex Rivera", total_amount=40.00, mandate={"user_id": "user-123", "service_description": "Dinner reservation for two", "date": "2027-03-10", "time": "19:00", "party_size": 2, "name_on_reservation": "Alex Rivera", "max_total_price": 60.00, "currency": "USD", "expires_at": "2027-03-09T00:00:00Z"})
|
|
56
|
+
decision, detail = route_verdict(verdict) # proceed / fix / escalate
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The separate steps are `create_reservation_mandate(**fields)` and `verify_reservation(mandate_id, merchant, date, time, party_size, name_on_reservation, total_amount, ...)`.
|
|
60
|
+
|
|
61
|
+
## What to do with the verdict
|
|
62
|
+
|
|
63
|
+
`route_verdict` encodes the consumer rules of the Agent Correction Format, and they are deliberately strict:
|
|
64
|
+
|
|
65
|
+
- **MATCH → proceed.** Capture the payment (or issue the ticket).
|
|
66
|
+
- **BLOCK → fix, narrowly.** Fix only when every correction in the response is a known action that brings the transaction back inside the buyer's existing authorization. Apply exactly those corrections, then resubmit as a new check carrying the earlier check's `record_id` as `previous_record_id`, within the response's `max_attempts` budget.
|
|
67
|
+
- **REVIEW, unknown verdict, unknown action → escalate.** Stop and ask the buyer. Never guess, and never widen the buyer's authorization to make a verdict go away.
|
|
68
|
+
|
|
69
|
+
```python
|
|
70
|
+
decision, detail = route_verdict(first_verdict)
|
|
71
|
+
if decision == "fix":
|
|
72
|
+
# ... apply detail["corrections"] to the transaction, nothing else ...
|
|
73
|
+
second = client.check_cart(..., mandate_id=same_id, previous_record_id=detail["record_id"])
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
## Offline and mock runs
|
|
77
|
+
|
|
78
|
+
The client takes an optional `transport` callable, `(path, body) -> (status, dict)`, that replaces the HTTP layer entirely. Point it at canned verdicts and the full mandate → check → route → resubmit loop runs with no network and no API key traffic — that is how this package's own test suite works, and it is the recommended way to rehearse an integration before wiring a real key:
|
|
79
|
+
|
|
80
|
+
```python
|
|
81
|
+
def stub(path, body):
|
|
82
|
+
return 200, {"id": "m-1"} if "mandates" in path else {"verdict": "MATCH", "record_id": "r-1"}
|
|
83
|
+
client = ProveCartClient(api_key="test", transport=stub)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Calls made with a sandbox key against the live API behave exactly as in production and stay sealed under that key, separate from live data.
|
|
87
|
+
|
|
88
|
+
## Links
|
|
89
|
+
|
|
90
|
+
- Agents and certification: https://provecart.com/agents
|
|
91
|
+
- Correction format standard: https://provecart.com/standards/correction-format
|
|
92
|
+
- API reference: https://api.provecart.com/docs
|
|
93
|
+
|
|
94
|
+
ProveCart. Prove the cart. Patent pending.
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
provecart/__init__.py,sha256=5SHgwuwQaPFlDTkww-ed62aT8JqE8G8JVz54rgrcGRk,414
|
|
2
|
+
provecart/client.py,sha256=fvaI6Lee9UuZXYbd1RRuq3WpEFl9ta3Upz0GW-b8IyQ,13032
|
|
3
|
+
provecart-0.1.0.dist-info/METADATA,sha256=LylMshay6cBI7_zGjXRWOLehG_1EPZJF4cDK8sMXa9A,6473
|
|
4
|
+
provecart-0.1.0.dist-info/WHEEL,sha256=mOdXR9C3wMVov7cSY8PY-XCzo3SmmWCHvGQr5csJoao,91
|
|
5
|
+
provecart-0.1.0.dist-info/top_level.txt,sha256=MnpDHUcK6UZSnzmny5HzCV8-g1FQ6JbaSU6FPEUtybw,10
|
|
6
|
+
provecart-0.1.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
provecart
|