mirror of
https://github.com/Stirling-Tools/Stirling-PDF.git
synced 2026-09-02 21:03:34 +03:00
PAYG usage card: review follow-ups (avg/PDF, empty-state, unique wording) (#6967)
Small follow-up to #6957 addressing the three non-blocker findings from its review. **Draft / stacked on #6957** — the diff shows #6957's changes until it merges, then auto-narrows to just these 5 files. Mark ready + rebase onto `main` once #6957 lands. ### 1. avg-per-PDF no longer blends unsynced units over synced-only docs `avgCostMinor` now divides **synced** units (`spendUnitsThisPeriod`) by synced docs, so numerator and denominator cover the same population. Combined-billing `pendingUnits` (units-only, no doc count) previously inflated the average for linked-instance teams. The "meter units" figure still shows synced+pending (total current usage) — only the *average* is synced-only. ### 2. Empty-state: unsynced-only reads cleanly When `docs == 0` but there are pending meter units (combined-billing, nothing synced yet), the card showed a bare **"0 PDFs"** headline with a count-less summary and no split. It now shows a **"{n} meter units pending sync from linked instances"** note instead. New `unitsPending` i18n key + a `UnsyncedOnly` story. (Only reachable on the combined-billing path; pure-SaaS teams are unaffected.) ### 3. uniquePdfs wording is now accurate `document_fingerprint` is a hash of a charge's whole **input set**, so the same file reused across *different* groupings (standalone, then later in a merge `{A,B}`) counts per grouping — a close approximation of "unique PDFs", exact for the single-input common case. Softened the FE type doc + the `WalletLedgerEntry.document_fingerprint` javadoc to say so (no behaviour change; counting model unchanged). ### Verification FE typecheck / test / lint / format all clean; `:saas:compileJava` green. No behaviour change beyond #1 (avg) and #2 (empty-state copy); #3 is doc-only.
This commit is contained in:
@@ -87,9 +87,13 @@ public class WalletLedgerEntry implements Serializable {
|
||||
private Integer docCount = 1;
|
||||
|
||||
/**
|
||||
* SHA-256 of this entry's input file set; {@code COUNT(DISTINCT ...)} over a period gives
|
||||
* unique PDFs processed. {@code null} for aggregate/system entries (grants, linked-instance
|
||||
* sync) that don't map to a single document set.
|
||||
* SHA-256 of this entry's input file <em>set</em> (the charge's whole input list, sorted).
|
||||
* {@code COUNT(DISTINCT ...)} over a period approximates unique PDFs processed. Exact within a
|
||||
* run (a file's chain/split steps share the run's charge, so one fingerprint), and for the
|
||||
* single-input common case; but the <em>same</em> file reused across different groupings — e.g.
|
||||
* standalone, then later merged as {A,B} — yields different set-fingerprints and is counted
|
||||
* once per grouping. {@code null} for aggregate/system entries (grants, linked-instance sync)
|
||||
* that don't map to a single document set.
|
||||
*/
|
||||
@Column(name = "document_fingerprint", length = 64)
|
||||
private String documentFingerprint;
|
||||
|
||||
@@ -6266,6 +6266,8 @@ sizeMultiplier = "{{formatted}} PDFs used a size multiplier"
|
||||
summary = "{{unique}} unique · {{units}} meter units · {{avg}} avg per PDF"
|
||||
summaryNoRate = "{{unique}} unique · {{units}} meter units"
|
||||
unit = "PDFs"
|
||||
unitsPending_one = "{{units}} meter unit pending sync from linked instances"
|
||||
unitsPending_other = "{{units}} meter units pending sync from linked instances"
|
||||
|
||||
[portal.billing.spendLimit]
|
||||
adjustLimit = "Adjust limit"
|
||||
|
||||
@@ -43,3 +43,26 @@ export const Empty: Story = {
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
/** Combined-billing: zero synced PDFs but unsynced instance units → "pending sync" note (no 0-PDF summary). */
|
||||
export const UnsyncedOnly: Story = {
|
||||
args: {
|
||||
wallet: {
|
||||
...subscribedWallet,
|
||||
billableUsed: 0,
|
||||
spendUnitsThisPeriod: 0,
|
||||
categoryBreakdown: { api: 0, ai: 0, automation: 0 },
|
||||
categoryDocs: { api: 0, ai: 0, automation: 0 },
|
||||
docsProcessedThisPeriod: 0,
|
||||
uniquePdfsThisPeriod: 0,
|
||||
sizeMultiplierPdfsThisPeriod: 0,
|
||||
},
|
||||
unsynced: {
|
||||
periodStart: subscribedWallet.billingPeriodStart,
|
||||
apiUnsyncedUnits: 8,
|
||||
aiUnsyncedUnits: 0,
|
||||
automationUnsyncedUnits: 4,
|
||||
totalUnsyncedUnits: 12,
|
||||
},
|
||||
},
|
||||
};
|
||||
|
||||
@@ -73,17 +73,15 @@ export function PdfsProcessedCard({
|
||||
const perDocs: WalletCategoryBreakdown = wallet.categoryDocs;
|
||||
const totalDocs = perDocs.api + perDocs.ai + perDocs.automation;
|
||||
|
||||
// Average cost per PDF in minor currency units — meter units × the per-unit rate,
|
||||
// spread over the input files processed. Shown only when the rate is known (free-tier
|
||||
// and unknown-price snapshots omit the term rather than imply $0.00).
|
||||
// Average cost per PDF in minor currency units — SYNCED units × the per-unit rate,
|
||||
// spread over the (synced) input files. Deliberately excludes combined-billing
|
||||
// pendingUnits: those are units-only (no doc count), so dividing them over synced docs
|
||||
// would inflate the average. Numerator and denominator therefore cover the same
|
||||
// population. Shown only when the rate is known (free-tier / unknown-price omit it).
|
||||
const rate = wallet.pricePerDocMinor;
|
||||
const showAvgCost = docs > 0 && rate != null;
|
||||
const avgCostMinor =
|
||||
rate != null && docs > 0 ? (meterUnits / docs) * rate : 0;
|
||||
|
||||
// Something ran once there are either counted PDFs or metered units (instance-local
|
||||
// unsynced usage is units-only, so it keeps the card out of the empty state).
|
||||
const hasActivity = docs > 0 || meterUnits > 0;
|
||||
rate != null && docs > 0 ? (wallet.spendUnitsThisPeriod / docs) * rate : 0;
|
||||
|
||||
return (
|
||||
<Card padding="loose">
|
||||
@@ -100,7 +98,7 @@ export function PdfsProcessedCard({
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{hasActivity ? (
|
||||
{docs > 0 ? (
|
||||
<>
|
||||
<p className="portal-billing__section-sub">
|
||||
{showAvgCost
|
||||
@@ -182,6 +180,18 @@ export function PdfsProcessedCard({
|
||||
</p>
|
||||
) : null}
|
||||
</>
|
||||
) : pendingUnits > 0 ? (
|
||||
// docs == 0 but instance-local units have accrued that SaaS hasn't billed yet
|
||||
// (combined-billing, units-only, no PDF count). Surface the pending figure directly
|
||||
// instead of a bare "0 PDFs" headline with a count-less summary + no split. Gated on
|
||||
// pendingUnits (not meterUnits) so the "pending sync" wording is always exact.
|
||||
<p className="portal-billing__section-sub">
|
||||
{t(
|
||||
"portal.billing.pdfsProcessed.unitsPending",
|
||||
"{{units}} meter units pending sync from linked instances",
|
||||
{ count: pendingUnits, units: pendingUnits.toLocaleString() },
|
||||
)}
|
||||
</p>
|
||||
) : (
|
||||
<p className="portal-billing__section-sub">
|
||||
{t(
|
||||
|
||||
@@ -66,7 +66,13 @@ export interface Wallet {
|
||||
categoryDocs: WalletCategoryBreakdown;
|
||||
/** Total input files processed this period (Σ doc_count). */
|
||||
docsProcessedThisPeriod: number;
|
||||
/** Distinct input documents this period — a file hit by N operations counts once. */
|
||||
/**
|
||||
* Distinct input document-sets this period — `COUNT(DISTINCT document_fingerprint)`, where the
|
||||
* fingerprint is the hash of a charge's whole input set. A file processed repeatedly within one
|
||||
* run (chain/split) counts once; the same file reused across *different* groupings (e.g.
|
||||
* standalone, then later in a merge {A,B}) has different fingerprints and so counts per grouping.
|
||||
* A close approximation of "unique PDFs", exact for the single-input common case.
|
||||
*/
|
||||
uniquePdfsThisPeriod: number;
|
||||
/** Input files on charges where the size multiplier applied (units billed > input files). */
|
||||
sizeMultiplierPdfsThisPeriod: number;
|
||||
|
||||
Reference in New Issue
Block a user