zeroclick-sellers 0.3.0 → 0.4.0
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.
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 21a687776a099cffab368909929c6181de3425e83d6fe94bcfaba52cc40e76a8
|
|
4
|
+
data.tar.gz: 15b009873e6410a7703a6385ebdfc0d737f1be8d3d475b18ed22e96683117904
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 10cc75f795e921fa55d77b5dce9f101d1f3c4afadb191fe70546db9bd01dd309fe8453cfedd24de1d1991e1716567de26378b17cb87fb8338866f1132d4e96bd
|
|
7
|
+
data.tar.gz: 82a1860e38b1a2b53195b1aa0c17830b60f2c7ad5cdc80efc280c6547678f74771ed799a42ddabaf405c1df3f00f36de6344402e3c104c03078cc94e2d00f90e
|
|
@@ -6,7 +6,9 @@ require_relative "money"
|
|
|
6
6
|
module ZeroClick
|
|
7
7
|
module Sellers
|
|
8
8
|
module Stateful
|
|
9
|
-
|
|
9
|
+
# suspended is recoverable; closed is terminal — revoke live keys, stop
|
|
10
|
+
# serving, keep your records.
|
|
11
|
+
ACCESS_STATUSES = %w[active suspended closed].freeze
|
|
10
12
|
|
|
11
13
|
# zod v4's `iso.datetime({offset: true})` grammar, transcribed verbatim —
|
|
12
14
|
# as the Python and Go SDKs also transcribe it. Seconds and the fraction
|
|
@@ -31,8 +33,8 @@ module ZeroClick
|
|
|
31
33
|
ParsedEntitlement = Struct.new(
|
|
32
34
|
:access_id, :agent_id, :buyer_id, :buyer_email, :idempotency_key,
|
|
33
35
|
:state_version, :plan, :period, :status,
|
|
34
|
-
:
|
|
35
|
-
:base_price_usd_micros, :
|
|
36
|
+
:lifetime_credit_granted_usd, :lifetime_credit_reversed_usd,
|
|
37
|
+
:base_price_usd_micros, :lifetime_credit_granted_usd_micros, :lifetime_credit_reversed_usd_micros,
|
|
36
38
|
keyword_init: true
|
|
37
39
|
)
|
|
38
40
|
|
|
@@ -60,9 +62,11 @@ module ZeroClick
|
|
|
60
62
|
nil
|
|
61
63
|
end
|
|
62
64
|
|
|
63
|
-
#
|
|
65
|
+
# Required but nullable: a missing key is an error, null is not.
|
|
64
66
|
# Returns [value, ok].
|
|
65
|
-
def
|
|
67
|
+
def nullable_id(container, key)
|
|
68
|
+
return [nil, false] unless container.key?(key)
|
|
69
|
+
|
|
66
70
|
value = container[key]
|
|
67
71
|
return [nil, true] if value.nil?
|
|
68
72
|
return [value, true] if non_empty_string?(value)
|
|
@@ -73,7 +77,7 @@ module ZeroClick
|
|
|
73
77
|
def parse_mint_key_request(body)
|
|
74
78
|
return nil unless body.is_a?(Hash) && non_empty_string?(body["agentId"])
|
|
75
79
|
|
|
76
|
-
buyer_id, ok =
|
|
80
|
+
buyer_id, ok = nullable_id(body, "buyerId")
|
|
77
81
|
return nil unless ok
|
|
78
82
|
|
|
79
83
|
MintKeyRequest.new(agent_id: body["agentId"], buyer_id: buyer_id)
|
|
@@ -118,9 +122,9 @@ module ZeroClick
|
|
|
118
122
|
AccessPeriod.new(start: start, end: finish)
|
|
119
123
|
end
|
|
120
124
|
|
|
121
|
-
def parse_money_field(container, key,
|
|
125
|
+
def parse_money_field(container, key, issues:)
|
|
122
126
|
unless container.key?(key)
|
|
123
|
-
issues << "state.#{key}: required (may be null)"
|
|
127
|
+
issues << "state.#{key}: required (may be null)"
|
|
124
128
|
return nil
|
|
125
129
|
end
|
|
126
130
|
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require "time"
|
|
4
|
+
|
|
3
5
|
require_relative "../errors"
|
|
4
6
|
require_relative "contracts"
|
|
5
7
|
require_relative "money"
|
|
@@ -7,8 +9,8 @@ require_relative "money"
|
|
|
7
9
|
module ZeroClick
|
|
8
10
|
module Sellers
|
|
9
11
|
module Stateful
|
|
10
|
-
StoredCreditGrant = Struct.new(:state_version, :
|
|
11
|
-
keyword_init: true)
|
|
12
|
+
StoredCreditGrant = Struct.new(:state_version, :lifetime_credit_granted_usd_micros,
|
|
13
|
+
:lifetime_credit_reversed_usd_micros, keyword_init: true)
|
|
12
14
|
|
|
13
15
|
class ParseOk
|
|
14
16
|
attr_reader :entitlement
|
|
@@ -74,10 +76,10 @@ module ZeroClick
|
|
|
74
76
|
issues << "#{key}: expected a non-empty string" unless Contracts.non_empty_string?(body[key])
|
|
75
77
|
end
|
|
76
78
|
|
|
77
|
-
buyer_id, buyer_ok = Contracts.
|
|
78
|
-
issues << "buyerId:
|
|
79
|
-
buyer_email, buyer_email_ok = Contracts.
|
|
80
|
-
issues << "buyerEmail:
|
|
79
|
+
buyer_id, buyer_ok = Contracts.nullable_id(body, "buyerId")
|
|
80
|
+
issues << "buyerId: required, a non-empty string or null" unless buyer_ok
|
|
81
|
+
buyer_email, buyer_email_ok = Contracts.nullable_id(body, "buyerEmail")
|
|
82
|
+
issues << "buyerEmail: required, a non-empty string or null" unless buyer_email_ok
|
|
81
83
|
|
|
82
84
|
state_version = Contracts.as_version(body["stateVersion"])
|
|
83
85
|
issues << "stateVersion: expected a non-negative integer" if state_version.nil?
|
|
@@ -96,11 +98,11 @@ module ZeroClick
|
|
|
96
98
|
period = Contracts.parse_period(state["period"], period_issues)
|
|
97
99
|
issues.concat(period_issues)
|
|
98
100
|
|
|
99
|
-
credit_granted = Contracts.parse_money_field(state, "
|
|
100
|
-
credit_reversed = Contracts.parse_money_field(state, "
|
|
101
|
+
credit_granted = Contracts.parse_money_field(state, "lifetimeCreditGrantedUsd", issues: issues)
|
|
102
|
+
credit_reversed = Contracts.parse_money_field(state, "lifetimeCreditReversedUsd", issues: issues)
|
|
101
103
|
|
|
102
104
|
status = state["status"]
|
|
103
|
-
issues << "state.status: expected active or
|
|
105
|
+
issues << "state.status: expected active, suspended, or closed" unless ACCESS_STATUSES.include?(status)
|
|
104
106
|
|
|
105
107
|
return ParseFailure.new(issues) unless issues.empty?
|
|
106
108
|
|
|
@@ -116,14 +118,25 @@ module ZeroClick
|
|
|
116
118
|
buyer_id: buyer_id, buyer_email: buyer_email,
|
|
117
119
|
idempotency_key: body["idempotencyKey"], state_version: state_version,
|
|
118
120
|
plan: plan, period: period, status: status,
|
|
119
|
-
|
|
121
|
+
lifetime_credit_granted_usd: credit_granted, lifetime_credit_reversed_usd: credit_reversed,
|
|
120
122
|
base_price_usd_micros: base_price_micros,
|
|
121
|
-
|
|
122
|
-
|
|
123
|
+
lifetime_credit_granted_usd_micros: Money.parse_money_usd(credit_granted),
|
|
124
|
+
lifetime_credit_reversed_usd_micros: Money.parse_money_usd(credit_reversed)
|
|
123
125
|
)
|
|
124
126
|
)
|
|
125
127
|
end
|
|
126
128
|
|
|
129
|
+
# The one request-time gate: serve only while the account is active AND
|
|
130
|
+
# its paid period (when it has one) is still current. Checking status
|
|
131
|
+
# alone misses period-based revocation; checking the period alone
|
|
132
|
+
# misses suspension and closure.
|
|
133
|
+
def serviceable?(state, now = Time.now)
|
|
134
|
+
return false unless state.status == "active"
|
|
135
|
+
|
|
136
|
+
finish = state.period&.end
|
|
137
|
+
finish.nil? || Time.iso8601(finish) > now
|
|
138
|
+
end
|
|
139
|
+
|
|
127
140
|
# Never-regress: an entitlement is applied only when it is strictly
|
|
128
141
|
# newer than what was last applied. Equal versions are replays, which
|
|
129
142
|
# is what makes delivery retries safe.
|
|
@@ -166,30 +179,30 @@ module ZeroClick
|
|
|
166
179
|
def derive_credit_delta(stored, entitlement)
|
|
167
180
|
return CreditReplay.new if !stored.nil? && !should_apply?(stored.state_version, entitlement.state_version)
|
|
168
181
|
|
|
169
|
-
incoming = entitlement.
|
|
182
|
+
incoming = entitlement.lifetime_credit_granted_usd_micros
|
|
170
183
|
if incoming.nil?
|
|
171
184
|
return NoCreditDimension.new(
|
|
172
185
|
StoredCreditGrant.new(
|
|
173
186
|
state_version: entitlement.state_version,
|
|
174
|
-
|
|
175
|
-
|
|
187
|
+
lifetime_credit_granted_usd_micros: stored&.lifetime_credit_granted_usd_micros,
|
|
188
|
+
lifetime_credit_reversed_usd_micros: stored&.lifetime_credit_reversed_usd_micros
|
|
176
189
|
)
|
|
177
190
|
)
|
|
178
191
|
end
|
|
179
192
|
|
|
180
|
-
prior = stored_micros(stored&.
|
|
181
|
-
prior_reversed = stored_micros(stored&.
|
|
193
|
+
prior = stored_micros(stored&.lifetime_credit_granted_usd_micros, "credit")
|
|
194
|
+
prior_reversed = stored_micros(stored&.lifetime_credit_reversed_usd_micros, "reversal")
|
|
182
195
|
|
|
183
196
|
# max(), not assignment: a stale delivery carrying a smaller total
|
|
184
197
|
# must not walk the purse backwards.
|
|
185
198
|
next_credit = [prior, incoming].max
|
|
186
|
-
next_reversed = [prior_reversed, entitlement.
|
|
199
|
+
next_reversed = [prior_reversed, entitlement.lifetime_credit_reversed_usd_micros || 0].max
|
|
187
200
|
delta = (next_credit - prior) - (next_reversed - prior_reversed)
|
|
188
201
|
|
|
189
202
|
next_state = StoredCreditGrant.new(
|
|
190
203
|
state_version: entitlement.state_version,
|
|
191
|
-
|
|
192
|
-
|
|
204
|
+
lifetime_credit_granted_usd_micros: next_credit,
|
|
205
|
+
lifetime_credit_reversed_usd_micros: next_reversed
|
|
193
206
|
)
|
|
194
207
|
|
|
195
208
|
if delta.negative?
|