@tinkerlandai/longplay-react-native 0.10.18 → 0.10.19

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.
@@ -1,6 +1,8 @@
1
1
  package ai.tinkerland.indiana
2
2
 
3
3
  import org.json.JSONObject
4
+ import org.json.JSONArray
5
+ import java.security.MessageDigest
4
6
 
5
7
  /** Merchant refinements dispatch on generated DTO fields, not user JSON. */
6
8
  internal object IndianaMerchantCommerceSemantics {
@@ -8,6 +10,13 @@ internal object IndianaMerchantCommerceSemantics {
8
10
  fun has(vararg keys: String) = fields.containsAll(keys.toList())
9
11
  fun require(value: Boolean) { if (!value) throw IndianaClientException("invalid_response") }
10
12
  fun objects(key: String) = v.getJSONArray(key).let { rows -> (0 until rows.length()).map { rows.getJSONObject(it) } }
13
+ if (has("merchantTerms", "operationId", "currency")) v.optJSONObject("merchantTerms")?.let { terms ->
14
+ val action = if (has("provider", "lines")) "purchase" else if (has("revokes")) "refund" else "subscription_change"
15
+ val provider = if (v.has("provider")) v.getString("provider") else v.getJSONObject("execution").getString("provider"); val request = terms.getJSONObject("request")
16
+ require(provider == "merchant" && request.getString("operationId") == v.getString("operationId") && request.getString("action") == action && terms.getString("currency") == v.getString("currency"))
17
+ }
18
+ if (has("merchantQuoteId", "requestDigest", "quoteTermsDigest", "request")) prepared(v)
19
+ if (has("reference", "kind", "description", "quantity", "unit")) require(v.isNull("quantity") == v.isNull("unit"))
11
20
  if (has("configurationRevision", "connectionRevision", "lineItems", "catalogAmountMinor", "discount")) {
12
21
  val lines = objects("lineItems"); val total = v.getLong("catalogAmountMinor")
13
22
  require(lines.sumOf { it.getLong("totalMinor") } == total && (v.isNull("discount") || v.getJSONObject("discount").getLong("amountMinor") <= total) && lines.map { it.getString("priceId") }.distinct().size == lines.size)
@@ -17,7 +26,13 @@ internal object IndianaMerchantCommerceSemantics {
17
26
  if (has("merchantExecutionId", "scheduledFor", "status") || has("provider", "eventId", "subscription", "scheduledFor", "status")) require((v.getString("status") == "scheduled") == !v.isNull("scheduledFor"))
18
27
  if (has("merchantExecutionId", "merchantRefundId", "status") && v.getString("status") == "succeeded") require(!v.isNull("merchantRefundId"))
19
28
  if (has("lookup", "operationId", "customerId", "execution")) v.optJSONObject("execution")?.let { require(it.getString("operationId") == v.getString("operationId") && it.getString("customerId") == v.getString("customerId")) }
20
- if (has("scope", "operationId", "customerId", "confirmedAt", "initialExecutionAllowed", "instruction")) v.getJSONObject("instruction").let { require(it.getString("operationId") == v.getString("operationId") && it.getString("customerId") == v.getString("customerId")) }
29
+ if (has("scope", "operationId", "customerId", "confirmedAt", "initialExecutionAllowed", "instruction")) v.getJSONObject("instruction").let {
30
+ require(it.getString("operationId") == v.getString("operationId") && it.getString("customerId") == v.getString("customerId"))
31
+ v.optJSONObject("merchantTerms")?.let { terms ->
32
+ val request = terms.getJSONObject("request"); val subject = request.getJSONObject("subject")
33
+ require(request.getString("operationId") == v.getString("operationId") && subject.getString("customerId") == v.getString("customerId") && canonical(subject.getJSONObject("scope")) == canonical(v.getJSONObject("scope")) && request.getString("action") == it.getString("type") && terms.getString("currency") == it.getString("currency"))
34
+ }
35
+ }
21
36
  if (has("merchantSubscriptionId", "periodStartsAt", "periodEndsAt", "items")) {
22
37
  val items = objects("items")
23
38
  require(utcMillis(v.getString("periodEndsAt")) > utcMillis(v.getString("periodStartsAt")) && items.map { it.getString("priceId") }.distinct().size == items.size)
@@ -28,11 +43,59 @@ internal object IndianaMerchantCommerceSemantics {
28
43
  require(lines.map { it.getString("merchantLineId") }.distinct().size == lines.size)
29
44
  for (key in listOf("subtotalMinor", "discountMinor", "taxMinor", "totalMinor")) require(lines.sumOf { it.getLong(key) } == v.getLong(key))
30
45
  }
31
- if (has("merchantLineId", "subtotalMinor", "discountMinor", "taxMinor", "totalMinor")) {
46
+ if (has("merchantPriceId", "merchantLineId", "subtotalMinor", "discountMinor", "taxMinor", "totalMinor")) quoteLine(v)
47
+ else if (has("merchantLineId", "subtotalMinor", "discountMinor", "taxMinor", "totalMinor")) {
32
48
  val subtotal = v.getLong("subtotalMinor"); val discount = v.getLong("discountMinor"); val tax = v.getLong("taxMinor")
33
49
  require(subtotal - discount + tax == v.getLong("totalMinor") && (if (subtotal < 0) discount == 0L && tax == 0L else discount <= subtotal))
34
50
  }
35
51
  if (has("provider", "eventId", "merchantOrderId", "paymentStatus", "invoice") && v.getString("paymentStatus") == "paid") require(!v.isNull("invoice"))
36
52
  if (has("provider", "merchantInvoiceId", "cancelSubscriptions", "entitlementAction", "amountMinor") || has("provider", "eventId", "merchantRefundId", "amountMinor", "status")) require(v.getLong("amountMinor") > 0)
37
53
  }
54
+ private fun ensure(value: Boolean) { if (!value) throw IndianaClientException("invalid_response") }
55
+ private fun quote(value: String): String = buildString {
56
+ append('"')
57
+ for ((index, c) in value.withIndex()) when (c) {
58
+ '"' -> append("\\\"")
59
+ '\\' -> append("\\\\")
60
+ '\b' -> append("\\b")
61
+ '\u000c' -> append("\\f")
62
+ '\n' -> append("\\n")
63
+ '\r' -> append("\\r")
64
+ '\t' -> append("\\t")
65
+ else -> if (c.code < 32 || Character.isHighSurrogate(c) && (index + 1 == value.length || !Character.isLowSurrogate(value[index + 1])) || Character.isLowSurrogate(c) && (index == 0 || !Character.isHighSurrogate(value[index - 1]))) append("\\u%04x".format(c.code)) else append(c)
66
+ }
67
+ append('"')
68
+ }
69
+ private fun canonical(value: Any?): String = when (value) {
70
+ null, JSONObject.NULL -> "null"
71
+ is JSONObject -> value.keys().asSequence().sorted().joinToString(",", "{", "}") { quote(it) + ":" + canonical(value.get(it)) }
72
+ is JSONArray -> (0 until value.length()).joinToString(",", "[", "]") { canonical(value.get(it)) }
73
+ is String -> quote(value)
74
+ is Number -> value.toString().toBigDecimal().stripTrailingZeros().toPlainString()
75
+ else -> value.toString()
76
+ }
77
+ private fun digest(value: Any?): String = MessageDigest.getInstance("SHA-256").digest(canonical(value).toByteArray(Charsets.UTF_8)).joinToString("") { "%02x".format(it.toInt() and 255) }
78
+ private fun objects(v: JSONObject, key: String): List<JSONObject> = v.getJSONArray(key).let { rows -> (0 until rows.length()).map { rows.getJSONObject(it) } }
79
+ private fun quoteLine(v: JSONObject) {
80
+ ensure(v.isNull("taxMinor") == v.isNull("totalMinor"))
81
+ if (!v.isNull("totalMinor")) ensure(v.getLong("totalMinor") == v.getLong("subtotalMinor") - v.getLong("discountMinor") + v.getLong("taxMinor"))
82
+ }
83
+ private fun prepared(v: JSONObject) {
84
+ val request = v.getJSONObject("request"); val action = request.getString("action"); val recurring = !v.isNull("subscriptionTerms")
85
+ val unsigned = JSONObject(v.toString()); unsigned.remove("quoteTermsDigest")
86
+ ensure(v.getString("requestDigest") == digest(request) && v.getString("quoteTermsDigest") == digest(unsigned) && v.getString("currency") == request.getString("currency"))
87
+ ensure(utcMillis(v.getString("observedAt")) < utcMillis(v.getString("expiresAt")) && utcMillis(v.getString("expiresAt")) <= utcMillis(request.getString("expiresAt")))
88
+ ensure(action != "purchase" || (request.getString("mode") == "subscription") == recurring)
89
+ ensure(!recurring || !(action == "refund" || action == "subscription_change" && request.getJSONObject("change").getString("type") == "cancel"))
90
+ val lines = objects(v, "lines"); lines.forEach(::quoteLine)
91
+ for ((rows, field) in listOf(lines to "merchantLineId", objects(v,"sourceRevisions") to "reference", objects(v,"requiredConsents") to "reference")) ensure(rows.map { it.getString(field) }.distinct().size == rows.size)
92
+ val subtotal = lines.sumOf { it.getLong("subtotalMinor") }; val discount = lines.sumOf { it.getLong("discountMinor") }; val known = lines.all { !it.isNull("taxMinor") }; val tax = lines.sumOf { if (it.isNull("taxMinor")) 0L else it.getLong("taxMinor") }
93
+ ensure(subtotal == v.getLong("subtotalMinor") && discount == v.getLong("discountMinor") && !v.isNull("taxMinor") == known && v.isNull("taxMinor") == v.isNull("totalMinor"))
94
+ ensure(if (known) tax == v.getLong("taxMinor") && v.getLong("totalMinor") == subtotal - discount + tax else v.getBoolean("automaticTax"))
95
+ ensure(lines.isNotEmpty() || action == "subscription_change" && subtotal == 0L && discount == 0L && tax == 0L && !v.isNull("totalMinor") && v.getLong("totalMinor") == 0L)
96
+ ensure(action == "subscription_change" || subtotal >= 0 && (v.isNull("totalMinor") || v.getLong("totalMinor") >= 0))
97
+ if (action == "refund") ensure((request.getString("mode") == "partial") == request.has("amountMinor") && known && v.getLong("totalMinor") > 0 && (request.getString("mode") != "partial" || v.getLong("totalMinor") == request.getLong("amountMinor")))
98
+ for (effect in objects(v, "expectedEffects")) ensure(effect.isNull("quantity") == effect.isNull("unit"))
99
+ }
100
+
38
101
  }
@@ -2,5 +2,5 @@
2
2
  package ai.tinkerland.indiana
3
3
 
4
4
  object IndianaSDK {
5
- const val version = "0.10.18"
5
+ const val version = "0.10.19"
6
6
  }