Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,15 @@

## Unreleased

- All six server SDKs recognize the Ban Credit claim verdicts
`linked_device_interactive` and `referral_group_send` on claim reads and
`bansafe.claim` webhooks, and type claim evidence `linkedDeviceInteractive`
and `referralGroupSend`. Rust's closed verdict enums previously rejected
these values. Source compatibility: Rust exhaustive `match` on the verdict
enums and `ClaimEvidence` struct literals, Go unkeyed `BanSafeClaimEvidence`
literals and the C# `BanSafeClaimEvidence` constructor/Deconstruct need the
added members. Pinned to unmerged API #845 source `5ccd3dcb`; package
publication waits for that API change to merge and deploy.
- All six server SDKs expose optional automatic-pause `reason`/`code` and
optional recipient-skip `messageIndex`/`followUpId` from accepted API source
`9bb1e2ed`. Future reason/code strings and raw envelope fields are retained;
Expand Down
10 changes: 10 additions & 0 deletions contracts/README.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,15 @@
# Contract coverage

## Ban Credit claim waivers

`ban-credit-waivers.json` pins the `BanSafeClaim` and `BanSafeClaimPayload`
verdict enums and the `linkedDeviceInteractive` and `referralGroupSend` claim
evidence fields to API source `5ccd3dcb7016d87994bcf19c58f2cc148ac82428`
(polymorfa/polymorfa#845, unmerged). It is held with `published: false`.
`ban-credit-waivers-contract.test.ts` checks the TypeScript types against it;
each native package has a decoding test. No operation is added. Re-pin to the
merged API revision before publication.

## Campaign and custody additions

`campaign-custody-20261014.json` pins thirty machine-accessible operations and
Expand Down
72 changes: 72 additions & 0 deletions contracts/ban-credit-waivers.json
Original file line number Diff line number Diff line change
@@ -0,0 +1,72 @@
{
"repository": "polymorfa/polymorfa",
"sourceCommit": "5ccd3dcb7016d87994bcf19c58f2cc148ac82428",
Comment on lines +1 to +3

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P1 Badge Rebind and verify the documentation snippet catalog

This supplement and the package changes leave contracts/doc-snippets.json and scripts/doc-snippets/recipes.mjs bound to 32c75bd, whose tracked inputs match the parent commit but not this commit (5 added files and 24 changed files). sourceDigest() in scripts/doc-snippets/catalog.mjs rejects that inventory mismatch, so the required check:doc-snippets step in .github/workflows/ci.yml cannot pass once dependencies are installed. Rebind the catalog to the updated SDK source, regenerate it, and verify the emitted programs with all six native checkers as required by AGENTS.md.

AGENTS.md reference: AGENTS.md:L62-L66

Useful? React with 👍 / 👎.

"published": false,
"scope": "Ban Credit claim waivers: verdicts linked_device_interactive and referral_group_send, and claim evidence linkedDeviceInteractive and referralGroupSend (polymorfa/polymorfa#845, unmerged). Other SDK contracts retain their recorded source revisions.",
"sources": {
"platform": {
"sourcePath": "apps/api/docs/openapi.management.json",
"sha256": "bc15edfa17c674f3aed5a192624bacaa96984155d2cc072ce1938d67f28fffa9"
},
"messaging": {
"sourcePath": "apps/api/docs/openapi.json",
"sha256": "b8a96e1f31e33ed5c4b1fb59fa9939f7f05456ed583a7ac9c68cbb10a7e29eb6"
}
},
"verdicts": {
"BanSafeClaim": {
"family": "platform",
"enum": [
"other_device",
"customer_conduct",
"shared_network",
"ours",
"inconclusive",
"simultaneous_calls",
"linked_device_interactive",
"referral_group_send"
]
},
"BanSafeClaimPayload": {
"family": "messaging",
"enum": [
"other_device",
"customer_conduct",
"shared_network",
"ours",
"inconclusive",
"simultaneous_calls",
"linked_device_interactive",
"referral_group_send"
]
}
},
"evidence": {
"BanSafeClaimEvidence": {
"family": "platform",
"required": [
"attributionRuleVersion",
"windowDays",
"deviceEvidence",
"otherDevices",
"restrictedInWindow",
"criticalFindingDays",
"sharedConnection",
"measuredHours",
"simultaneousCalls",
"linkedDeviceInteractive",
"referralGroupSend"
],
"properties": {
"linkedDeviceInteractive": {
"type": "boolean",
"description": "Whether this number sent an interactive message (buttons, lists, product messages, Flows and similar) from a linked device inside the window. When true, Ban Credit does not cover the ban and the claim is denied when filed. False for claims filed before this was recorded."
},
"referralGroupSend": {
"type": "boolean",
"description": "Whether this number posted a referral message to WhatsApp groups inside the window. When true, Ban Credit does not cover the ban and the claim is denied when filed. False for claims filed before this was recorded."
}
}
}
}
}
7 changes: 7 additions & 0 deletions packages/dotnet/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -1010,6 +1010,13 @@ when the 30-day claim window overlaps an enabled period. Claim verdicts and
`bansafe.claim` webhook verdicts recognize `simultaneous_calls`. Claim evidence
includes `simultaneousCalls`; the webhook does not acquire an evidence object.

Ban Credit claims also recognize `linked_device_interactive` (the Number sent
an interactive message from a linked device in the 30 days before the ban) and
`referral_group_send` (it posted a referral message to WhatsApp groups in that
window), on claim reads and `bansafe.claim` webhooks. Both open `denied` with
amount 0. Claim evidence includes `linkedDeviceInteractive` and
`referralGroupSend`; claims filed before these were recorded report `false`.

No browser/client-token authority, call media behavior, automatic state read,
retry policy or feature availability is added by these SDK fields.

Expand Down
2 changes: 1 addition & 1 deletion packages/dotnet/src/Polymorfa.Sdk/BanSafe.cs
Original file line number Diff line number Diff line change
Expand Up @@ -64,7 +64,7 @@ public sealed record BanSafeEnforcementSummary : BanSafeNumber
public sealed record BanSafeIncident(string Id, string SessionId, string Session, string PhoneNumber, string ProjectId, string Kind, string Source, string Resolution, bool Ambiguous, string StartedAt, string? EndsAt, string? ClosedAt, string? ClosedBy, string? ClaimId, string? Note, string? ReportedBy, string CreatedAt);
public sealed record ReportBanSafeIncidentRequest(string Session, string? OccurredAt = null, string? Note = null);
public sealed record BanSafeIncidentReceipt(string IncidentId, bool Created, string SessionId, string Session, string OccurredAt);
public sealed record BanSafeClaimEvidence(long? AttributionRuleVersion, int WindowDays, bool DeviceEvidence, long OtherDevices, bool RestrictedInWindow, long CriticalFindingDays, bool SharedConnection, decimal MeasuredHours, bool SimultaneousCalls);
public sealed record BanSafeClaimEvidence(long? AttributionRuleVersion, int WindowDays, bool DeviceEvidence, long OtherDevices, bool RestrictedInWindow, long CriticalFindingDays, bool SharedConnection, decimal MeasuredHours, bool SimultaneousCalls, bool LinkedDeviceInteractive, bool ReferralGroupSend);
public sealed record BanSafeClaim(string Id, string IncidentId, string SessionId, string Session, string PhoneNumber, string ProjectId, string Status, string Verdict, string WindowStart, string WindowEnd, decimal MeasuredCents, decimal CapCents, decimal AmountCents, BanSafeClaimEvidence Evidence, string Summary, string Reason, string? DecidedAt, string? PaidAt, string CreatedAt);
public sealed record BanSafeSignalDefinition(string Key, string Label, string Group, string Kind, string Unit, string Description);
public sealed record BanSafeCodeCount(long Code, long Count);
Expand Down
16 changes: 15 additions & 1 deletion packages/dotnet/tests/Polymorfa.Sdk.Tests/BanSafeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,7 @@ internal static class BanSafeTests
{
private const string Health = """{"sessionId":"s1","session":"support","phoneNumber":"+15550001111","projectId":"p1","health":null,"band":"unknown","healthSource":"unavailable","healthEstimatorVersion":null,"healthModelVersion":null,"healthEvaluatedAt":null,"healthFeatureCoverage":null,"healthReliability":"unavailable","healthUnavailableReason":"no_active_model","healthProbabilities":null,"mostLikelyHealthState":null,"healthExplanation":null,"observedAccountState":{"state":"limited","observedAt":"2026-09-22","source":"restriction_event"},"enforcement":null}""";
private const string Incident = """{"id":"i1","sessionId":"s1","session":"support","phoneNumber":"+15550001111","projectId":"p1","kind":"customer_report","source":"customer","resolution":"retracted","ambiguous":true,"startedAt":"2026-09-22","endsAt":null,"closedAt":null,"closedBy":"owner","claimId":null,"note":null,"reportedBy":null,"createdAt":"2026-09-22"}""";
private const string Claim = """{"id":"c1","incidentId":"i1","sessionId":"s1","session":"support","phoneNumber":"+15550001111","projectId":"p1","status":"paid","verdict":"ours","windowStart":"2026-09-01","windowEnd":"2026-09-22","measuredCents":1.123456,"capCents":2.123456,"amountCents":0.123456,"evidence":{"attributionRuleVersion":1,"windowDays":21,"deviceEvidence":false,"otherDevices":0,"restrictedInWindow":false,"criticalFindingDays":0,"sharedConnection":false,"simultaneousCalls":false,"measuredHours":1.5},"summary":"Reviewed","reason":"ours","decidedAt":null,"paidAt":null,"createdAt":"2026-09-22"}""";
private const string Claim = """{"id":"c1","incidentId":"i1","sessionId":"s1","session":"support","phoneNumber":"+15550001111","projectId":"p1","status":"paid","verdict":"ours","windowStart":"2026-09-01","windowEnd":"2026-09-22","measuredCents":1.123456,"capCents":2.123456,"amountCents":0.123456,"evidence":{"attributionRuleVersion":1,"windowDays":21,"deviceEvidence":false,"otherDevices":0,"restrictedInWindow":false,"criticalFindingDays":0,"sharedConnection":false,"simultaneousCalls":false,"linkedDeviceInteractive":false,"referralGroupSend":false,"measuredHours":1.5},"summary":"Reviewed","reason":"ours","decidedAt":null,"paidAt":null,"createdAt":"2026-09-22"}""";
private const string Collection = """{"state":"fresh","latestFlushedAt":"2026-09-22","latestReceivedAt":"2026-09-22","freshUntil":null,"recordVersion":1,"collectorVersion":1,"partial":false,"droppedRecords":0}""";
private const string Snapshot = """{"bucketStart":"2026-09-22","flushedAt":"2026-09-22","receivedAt":"2026-09-22","partial":false,"recordVersion":1,"signals":[{"key":"latency","label":"Latency","group":"delivery","kind":"histogram","unit":"milliseconds","description":"Latency","measured":true,"value":[1.25,2.5],"sampleSize":2,"codes":null},{"key":"ready","label":"Ready","group":"connection","kind":"boolean","unit":"none","description":"Ready","measured":true,"value":false,"sampleSize":0,"codes":[{"code":403,"count":1}]}]}""";
public static async Task RunAsync()
Expand All @@ -26,6 +26,20 @@ public static async Task RunAsync()
await Organization("POST", "/platform/bansafe/incidents/i1/retract", null, Envelope(Incident), async c => ResourceTests.Equal((await c.BanSafe.RetractIncidentAsync("i1")).Data.Data.ClosedBy, "owner"));
await Organization("GET", "/platform/bansafe/claims?session=support&status=paid", null, Page(Claim), async c => ResourceTests.Equal((await c.BanSafe.ListClaimsAsync(new(Session: "support", Status: "paid"))).Items[0].AmountCents, 0.123456m));
await Organization("GET", "/platform/bansafe/claims/c1", null, Envelope(Claim), async c => ResourceTests.Equal((await c.BanSafe.GetClaimAsync("c1")).Data.Data.Evidence.MeasuredHours, 1.5m));
// Ban Credit waivers: both verdicts and evidence flags decode from the claim read.
foreach (var (verdict, linked, referral) in new[] { ("linked_device_interactive", true, false), ("referral_group_send", false, true) })
{
var waived = Claim.Replace("\"status\":\"paid\",\"verdict\":\"ours\"", $"\"status\":\"denied\",\"verdict\":\"{verdict}\"").Replace("\"attributionRuleVersion\":1", "\"attributionRuleVersion\":3").Replace("\"linkedDeviceInteractive\":false,\"referralGroupSend\":false", $"\"linkedDeviceInteractive\":{(linked ? "true" : "false")},\"referralGroupSend\":{(referral ? "true" : "false")}");
await Organization("GET", "/platform/bansafe/claims/c1", null, Envelope(waived), async c =>
{
var claim = (await c.BanSafe.GetClaimAsync("c1")).Data.Data;
ResourceTests.Equal(claim.Verdict, verdict);
ResourceTests.Equal(claim.Evidence.AttributionRuleVersion, 3L);
ResourceTests.True(claim.Evidence.LinkedDeviceInteractive == linked && claim.Evidence.ReferralGroupSend == referral);
});
var payload = JsonSerializer.Deserialize<BanSafeClaimPayload>($$"""{"id":"c1","incidentId":"i1","phoneNumber":"+15550001111","status":"denied","verdict":"{{verdict}}","windowStart":"a","windowEnd":"b","measuredCents":0,"capCents":0,"amountCents":0,"summary":"s","reason":"r","decidedAt":null,"paidAt":null}""", new JsonSerializerOptions(JsonSerializerDefaults.Web))!;
ResourceTests.Equal(payload.Verdict, verdict);
}
var options = new JsonSerializerOptions(); foreach (var json in new[] { "null", "0", "false", "\"unknown\"", "[]", "[1.5,2.25]" }) { var v = JsonSerializer.Deserialize<BanSafeSignalValue>(json, options); ResourceTests.Equal(JsonSerializer.Serialize(v, options), json); }
foreach (var json in new[] { "{}", "[true]" }) { try { JsonSerializer.Deserialize<BanSafeSignalValue>(json, options); throw new Exception("Invalid signal value accepted"); } catch (JsonException) { } catch (InvalidOperationException) { } }
Console.WriteLine("PASS 15 BanSafe routes, health authority/nulls, closed signal variants and six-decimal claims");
Expand Down
2 changes: 1 addition & 1 deletion packages/dotnet/tests/Polymorfa.Sdk.Tests/CallsTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ public static async Task RunAsync()
await Invalid(() => client.Calls.UpdateCallSettingsAsync("s", new(AcknowledgeRisk: true)));
await Invalid(() => client.Calls.ReportAsync("call", new("quality", "connection1", new("polymorfa-dotnet", "0.1.0", "other"), Quality: new(RttMs: -1))));
var json = new System.Text.Json.JsonSerializerOptions { PropertyNamingPolicy = System.Text.Json.JsonNamingPolicy.CamelCase };
var claim = System.Text.Json.JsonSerializer.Deserialize<BanSafeClaimEvidence>("""{"attributionRuleVersion":null,"windowDays":30,"deviceEvidence":false,"otherDevices":0,"restrictedInWindow":false,"criticalFindingDays":0,"sharedConnection":false,"simultaneousCalls":true,"measuredHours":0}""", json)!;
var claim = System.Text.Json.JsonSerializer.Deserialize<BanSafeClaimEvidence>("""{"attributionRuleVersion":null,"windowDays":30,"deviceEvidence":false,"otherDevices":0,"restrictedInWindow":false,"criticalFindingDays":0,"sharedConnection":false,"simultaneousCalls":true,"linkedDeviceInteractive":false,"referralGroupSend":false,"measuredHours":0}""", json)!;
True(claim.SimultaneousCalls);
Console.WriteLine("PASS Calls routes, simultaneous-call presence, evidence and validation");
}
Expand Down
2 changes: 2 additions & 0 deletions packages/dotnet/tests/Polymorfa.Sdk.Tests/Program.cs
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@

if (args.Contains("--simultaneous-calls-only")) { await CallsTests.RunAsync(); return; }

if (args.Contains("--bansafe-only")) { await BanSafeTests.RunAsync(); Console.WriteLine("PASS BanSafe routes and Ban Credit waiver claims"); return; }

if (args.Contains("--official-round2-only")) { await OfficialRound2Tests.RunAsync(); return; }

OpaqueCredentialTests.Run();
Expand Down
7 changes: 7 additions & 0 deletions packages/go/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,13 @@ when the 30-day claim window overlaps an enabled period. Claim verdicts and
`bansafe.claim` webhook verdicts recognize `simultaneous_calls`. Claim evidence
includes `simultaneousCalls`; the webhook does not acquire an evidence object.

Ban Credit claims also recognize `linked_device_interactive` (the Number sent
an interactive message from a linked device in the 30 days before the ban) and
`referral_group_send` (it posted a referral message to WhatsApp groups in that
window), on claim reads and `bansafe.claim` webhooks. Both open `denied` with
amount 0. Claim evidence includes `linkedDeviceInteractive` and
`referralGroupSend`; claims filed before these were recorded report `false`.

No browser/client-token authority, call media behavior, automatic state read,
retry policy or feature availability is added by these SDK fields.

Expand Down
41 changes: 41 additions & 0 deletions packages/go/ban_credit_waivers_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
package polymorfa

import (
"encoding/json"
"testing"
)

func TestBanCreditWaiverVerdictsAndEvidence(t *testing.T) {
for _, tc := range []struct {
verdict string
evidence string
linked bool
referral bool
}{
{"linked_device_interactive", `{"attributionRuleVersion":3,"linkedDeviceInteractive":true,"referralGroupSend":false}`, true, false},
{"referral_group_send", `{"attributionRuleVersion":3,"linkedDeviceInteractive":false,"referralGroupSend":true}`, false, true},
} {
var claim BanSafeClaim
if err := json.Unmarshal([]byte(`{"status":"denied","amountCents":0,"verdict":"`+tc.verdict+`","evidence":`+tc.evidence+`}`), &claim); err != nil {
t.Fatal(err)
}
if claim.Verdict != tc.verdict || claim.Evidence.AttributionRuleVersion == nil || *claim.Evidence.AttributionRuleVersion != 3 {
t.Fatalf("decoded %+v", claim)
}
if claim.Evidence.LinkedDeviceInteractive != tc.linked || claim.Evidence.ReferralGroupSend != tc.referral {
t.Fatalf("evidence %+v", claim.Evidence)
}
encoded, err := json.Marshal(claim.Evidence)
if err != nil {
t.Fatal(err)
}
var round map[string]any
if err := json.Unmarshal(encoded, &round); err != nil || round["linkedDeviceInteractive"] != tc.linked || round["referralGroupSend"] != tc.referral {
t.Fatalf("round trip %s", encoded)
}
var payload BanSafeClaimPayload
if err := json.Unmarshal([]byte(`{"verdict":"`+tc.verdict+`"}`), &payload); err != nil || payload.Verdict != tc.verdict {
t.Fatalf("webhook payload %+v %v", payload, err)
}
}
}
20 changes: 11 additions & 9 deletions packages/go/bansafe.go
Original file line number Diff line number Diff line change
Expand Up @@ -182,15 +182,17 @@ type BanSafeIncidentReceipt struct {
OccurredAt string `json:"occurredAt"`
}
type BanSafeClaimEvidence struct {
SimultaneousCalls bool `json:"simultaneousCalls"`
AttributionRuleVersion *int `json:"attributionRuleVersion"`
WindowDays int `json:"windowDays"`
DeviceEvidence bool `json:"deviceEvidence"`
OtherDevices int `json:"otherDevices"`
RestrictedInWindow bool `json:"restrictedInWindow"`
CriticalFindingDays int `json:"criticalFindingDays"`
SharedConnection bool `json:"sharedConnection"`
MeasuredHours int `json:"measuredHours"`
SimultaneousCalls bool `json:"simultaneousCalls"`
LinkedDeviceInteractive bool `json:"linkedDeviceInteractive"`
ReferralGroupSend bool `json:"referralGroupSend"`
AttributionRuleVersion *int `json:"attributionRuleVersion"`
WindowDays int `json:"windowDays"`
DeviceEvidence bool `json:"deviceEvidence"`
OtherDevices int `json:"otherDevices"`
RestrictedInWindow bool `json:"restrictedInWindow"`
CriticalFindingDays int `json:"criticalFindingDays"`
SharedConnection bool `json:"sharedConnection"`
MeasuredHours int `json:"measuredHours"`
}
type BanSafeClaim struct {
ID string `json:"id"`
Expand Down
7 changes: 7 additions & 0 deletions packages/php/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -960,6 +960,13 @@ when the 30-day claim window overlaps an enabled period. Claim verdicts and
`bansafe.claim` webhook verdicts recognize `simultaneous_calls`. Claim evidence
includes `simultaneousCalls`; the webhook does not acquire an evidence object.

Ban Credit claims also recognize `linked_device_interactive` (the Number sent
an interactive message from a linked device in the 30 days before the ban) and
`referral_group_send` (it posted a referral message to WhatsApp groups in that
window), on claim reads and `bansafe.claim` webhooks. Both open `denied` with
amount 0. Claim evidence includes `linkedDeviceInteractive` and
`referralGroupSend`; claims filed before these were recorded report `false`.

No browser/client-token authority, call media behavior, automatic state read,
retry policy or feature availability is added by these SDK fields.

Expand Down
Loading
Loading