Skip to content
Closed
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
Original file line number Diff line number Diff line change
Expand Up @@ -43,6 +43,7 @@ import {
canonicalFullFiscalYearPlanPeriods,
isCanonicalFullFiscalYearPeriodPlan,
} from "./filed-returns-full-fiscal-year-validation";
import { filedReturnsTargetStatusBehaviour } from "../connectors/gst/filed-returns-contracts";

export const ALL_SUPPORTED_FULL_FISCAL_YEAR_PLAN_VERSION =
"all-supported-filed-returns-targets-v1" as const;
Expand Down Expand Up @@ -522,10 +523,13 @@ function isTarget(
) {
return false;
}
return (
verifiedTarget.status !== "not-filed" ||
verifiedTarget.safeSignals.includes("filed-return-positively-not-filed")
);
// The evidence a claim needs is a property of the status, not a rule each validator remembers.
// Spelled out, only `not-filed` was checked here and in the single-return validator, so a stored
// record could assert `not-generated` with nothing behind it -- in both.
const requiredEvidenceSignal = filedReturnsTargetStatusBehaviour(
verifiedTarget.status,
).requiredEvidenceSignal;
return !requiredEvidenceSignal || verifiedTarget.safeSignals.includes(requiredEvidenceSignal);
}

function hasCanonicalConcreteArtifacts(
Expand Down
16 changes: 5 additions & 11 deletions src/background/filed-returns-current-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@ import {
readRetainedPlanLedgers,
} from "./filed-returns-full-fiscal-year-run-state";
import { readCurrentFiledReturnsTargetReviewSummary } from "./filed-returns-target-review";
import { isResolvedFullFiscalYearTargetStatus } from "../connectors/gst/filed-returns-contracts";

export interface FiledReturnsCurrentStateDeps {
storageKeys: {
Expand Down Expand Up @@ -143,17 +144,10 @@ function isRetainedZipRetrySummary(

function isActionableFullFiscalYearLedger(ledger: FiledReturnsFullFiscalYearLedger): boolean {
if (ledger.status === "complete") return false;
return ledger.targets.some((target) =>
[
"pending",
"running",
"download-unconfirmed",
"blocked",
"failed",
"cancelled",
"manually-observed",
].includes(target.status),
);
// The complement of "answered", asked as such. This was a hand-written list of seven members,
// and `local-data.ts` kept a second copy of the same idea that had six -- so a cancelled target
// made a ledger actionable here while leaving it clearable there.
return ledger.targets.some((target) => !isResolvedFullFiscalYearTargetStatus(target.status));
}

function isNewerSinglePeriodSummary(
Expand Down
11 changes: 5 additions & 6 deletions src/background/filed-returns-full-fiscal-year-run-state.ts
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,6 @@ import type {
FiledReturnsFullFiscalYearLedger,
PortalFlowStepResult,
} from "../connectors/gst/filed-returns-contracts";
import { isResolvedFullFiscalYearTargetStatus } from "../connectors/gst/filed-returns-contracts";
import { isCleanedZipPhase } from "../connectors/gst/filed-returns-contracts";
import type { PackMessageResponse } from "../connectors/gst/messages";
import { filedReturnScopeId } from "../connectors/gst/filed-returns-return-descriptors";
import { normaliseFiledReturnsArtifactType } from "../connectors/gst/filed-returns-artifacts";
Expand All @@ -32,15 +30,16 @@ import {
toFullFiscalYearSummary,
} from "./filed-returns-full-fiscal-year-summary";
import { persistCanonicalFiledReturnsFlowSummary } from "./filed-returns-session-summary";
import {
holdsFullFiscalYearTargetAnswer,
isCleanedZipPhase,
} from "../connectors/gst/filed-returns-contracts";

// A target the run should not silently discard: the portal answered, or a person reported what they
// saw. Written as a list, this said `not-filed` but not `not-generated` -- two answers of the same
// kind, one of which would have let a cancelled run holding it be replaced without asking.
export function hasTerminalPositiveTarget(ledger: FiledReturnsFullFiscalYearLedger): boolean {
return ledger.targets.some(
(target) =>
isResolvedFullFiscalYearTargetStatus(target.status) || target.status === "manually-observed",
);
return ledger.targets.some((target) => holdsFullFiscalYearTargetAnswer(target.status));
}

export function hasDownloadUnconfirmedTarget(ledger: FiledReturnsFullFiscalYearLedger): boolean {
Expand Down
3 changes: 2 additions & 1 deletion src/background/filed-returns-full-fiscal-year-staging.ts
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,7 @@ import {
import { durableFullFiscalYearArtifactSignals } from "./filed-returns-full-fiscal-year-validation";
import { discardFullFiscalYearFiledReturnsZip } from "./filed-returns-full-fiscal-year-zip";
import { readCanonicalFiledReturnsFlowSummary } from "./filed-returns-session-summary";
import { statesFullFiscalYearTargetAbsence } from "../connectors/gst/filed-returns-contracts";

const FULL_YEAR_STAGED_SIGNAL_PREFIX = "full-fiscal-year-opfs-staged:";

Expand Down Expand Up @@ -267,7 +268,7 @@ export function markFullFiscalYearRestagingRequired(
): FiledReturnsFullFiscalYearLedger {
const timestamp = now.toISOString();
const targets = ledger.targets.map((target) =>
target.status === "not-filed"
statesFullFiscalYearTargetAbsence(target.status)
? target
: {
...target,
Expand Down
5 changes: 3 additions & 2 deletions src/background/filed-returns-full-fiscal-year-summary.ts
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import {
hasInconsistentFullFiscalYearCompletion,
isFullFiscalYearLedgerStale,
} from "./filed-returns-full-fiscal-year-ledger";
import { statesFullFiscalYearTargetAbsence } from "../connectors/gst/filed-returns-contracts";

export function fullFiscalYearZipPhaseStep(
ledger: FiledReturnsFullFiscalYearLedger,
Expand Down Expand Up @@ -330,7 +331,7 @@ export function fullFiscalYearTargetEvidence(
// run produced. Everything that depended on a file Pack no longer has goes.
if (runDiscarded || (clearedWithoutDelivery && hadStagedFiles)) {
return ledger.targets
.filter((target) => target.status === "not-filed")
.filter((target) => statesFullFiscalYearTargetAbsence(target.status))
.map((target) => ({ period: target.period, outcome: "not-filed" as const }));
Comment thread
lamemustafa marked this conversation as resolved.
}
// From the step as well as the ledger. An MV3 interruption produces a blocked
Expand Down Expand Up @@ -459,7 +460,7 @@ export function completeFullFiscalYearStep(
...(unplanned.length > 0 ? ["full-fiscal-year-plan-narrower-than-eligible"] : []),
...(ledger.zipPhase === "cleaned-without-export" &&
ledger.targets.length > 0 &&
ledger.targets.every((target) => target.status === "not-filed")
ledger.targets.every((target) => statesFullFiscalYearTargetAbsence(target.status))
? ["full-fiscal-year-no-zip-artifacts"]
: []),
],
Expand Down
12 changes: 8 additions & 4 deletions src/background/filed-returns-full-fiscal-year-validation.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ import {
hasPositiveFiledReturnsDownloadEvidence,
isValidFiledReturnsDownloadDiagnosticState,
} from "./filed-returns-download-diagnostic-state";
import { filedReturnsTargetStatusBehaviour } from "../connectors/gst/filed-returns-contracts";

export const FULL_FISCAL_YEAR_PLAN_VERSION = "filed-returns-targets-v3";

Expand Down Expand Up @@ -441,10 +442,13 @@ function isFullFiscalYearTarget(
) {
return false;
}
if (
target.status === "not-filed" &&
!target.safeSignals?.includes("filed-return-positively-not-filed")
) {
// A status is a claim, and the evidence each claim needs is a property of the status rather
// than a rule this file remembers. Spelled out here, only `not-filed` was ever checked, so a
// stored record could assert `not-generated` with nothing behind it.
const requiredEvidenceSignal = filedReturnsTargetStatusBehaviour(
target.status,
).requiredEvidenceSignal;
if (requiredEvidenceSignal && !target.safeSignals?.includes(requiredEvidenceSignal)) {
return false;
}
return true;
Expand Down
19 changes: 8 additions & 11 deletions src/background/local-data.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
import { browser } from "wxt/browser";
import type { FiledReturnsFullFiscalYearLedger } from "../connectors/gst/filed-returns-contracts";
import { isCleanedZipPhase } from "../connectors/gst/filed-returns-contracts";
import type { PackMessageResponse } from "../connectors/gst/messages";
import {
readActiveFiledReturnsRunStorageState,
Expand Down Expand Up @@ -29,6 +28,10 @@ import {
clearAllSupportedFullFiscalYearLedgerPlans,
readAllSupportedPlanLedgersStorageStateWithinOperation,
} from "./filed-returns-all-supported-full-fiscal-year-run-state";
import {
isCleanedZipPhase,
isResolvedFullFiscalYearTargetStatus,
} from "../connectors/gst/filed-returns-contracts";

export interface PackLocalDataDeps {
clearableLocalStorageKeys: readonly string[];
Expand Down Expand Up @@ -209,16 +212,10 @@ function hasUnresolvedZipState(ledger: {
function isUnresolvedFullFiscalYearLedger(ledger: FiledReturnsFullFiscalYearLedger): boolean {
if (hasInconsistentFullFiscalYearCompletion(ledger)) return true;
if (ledger.status === "complete" || ledger.status === "cancelled") return false;
return ledger.targets.some((target) =>
[
"pending",
"running",
"download-unconfirmed",
"blocked",
"failed",
"manually-observed",
].includes(target.status),
);
// The same question `filed-returns-current-state.ts` asks when it decides which ledger to show,
// and now the same answer. The two lists had drifted: this one omitted `cancelled`, so a ledger
// the panel was still surfacing could be cleared from under it.
return ledger.targets.some((target) => !isResolvedFullFiscalYearTargetStatus(target.status));
}

async function readLocalValue<T>(key: string): Promise<T | null> {
Expand Down
122 changes: 108 additions & 14 deletions src/connectors/gst/filed-returns-contracts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -258,34 +258,128 @@ export function isFiledReturnsFullFiscalYearTargetStatus(
return (FILED_RETURNS_FULL_FISCAL_YEAR_TARGET_STATUSES as readonly unknown[]).includes(value);
}

// A target the portal has answered for. Retrying one cannot change its outcome, so a run counts it
// as done. Nine modules each kept their own copy of this pair, every copy spelled `downloaded` and
// `not-filed`, and none of them learned about `not-generated` -- which is how a live run stopped on
// the first period the portal declined to draft.
const RESOLVED_TARGET_STATUSES = new Set<FiledReturnsFullFiscalYearTargetStatus>([
"downloaded",
"not-filed",
"not-generated",
]);
/**
* What a status *means*, in one place, answered for every member.
*
* The questions below were previously asked as literal comparisons scattered across twenty-two
* modules. Nothing connected them, so each was an independent place to forget: `not-generated` was
* added to the union and five separate sites went on spelling the question `=== "not-filed"`,
* silently answering "no" for every period the portal declined to draft.
*
* A `Record` over the union is what makes that impossible. Adding a member does not compile until
* every question below has been answered for it -- which is the difference between this and the
* `Set` that used to live here, under a comment warning against exactly the `Set`.
*/
interface FiledReturnsTargetStatusBehaviour {
/** The run is still working on this by itself. Nobody needs to decide anything. */
active: boolean;
/** The portal has answered. Retrying cannot change the outcome, so a run counts it as done. */
resolved: boolean;
/**
* The portal stated that no artifact exists for this period.
*
* Deliberately one question with two members behind it. `not-filed` is a claim about the
* taxpayer and `not-generated` is a claim about the portal, so they must stay distinct wherever
* a person reads them -- but "is there a file to expect?" has the same answer for both, and
* every site that asked it by naming only `not-filed` was wrong.
*/
statedAbsence: boolean;
/** A local file was staged for this target. Narrower than `resolved`: an absence stages nothing. */
producedFile: boolean;
/**
* Holds an answer a run must not discard or overwrite.
*
* Wider than `resolved`: a manually observed target was answered by a person rather than the
* portal, which does not resolve it but is still work that took a human and cannot be replaced.
*/
holdsAnswer: boolean;
/**
* The signal a stored record claiming this status must carry, or `null` where none applies.
*
* A status is a claim, and a claim without its evidence is how a ledger comes back asserting
* something no run established. `downloaded` is `null` here because its evidence is a richer
* predicate than a signal name -- see `hasPositiveFiledReturnsDownloadEvidence`.
*/
requiredEvidenceSignal: string | null;
}

const TARGET_STATUS_BEHAVIOUR: Readonly<
Record<FiledReturnsFullFiscalYearTargetStatus, FiledReturnsTargetStatusBehaviour>
> = {
pending: base({ active: true }),
running: base({ active: true }),
downloaded: base({ resolved: true, producedFile: true, holdsAnswer: true }),
"manually-observed": base({ holdsAnswer: true }),
"not-filed": base({
resolved: true,
statedAbsence: true,
holdsAnswer: true,
requiredEvidenceSignal: "filed-return-positively-not-filed",
}),
"not-generated": base({
resolved: true,
statedAbsence: true,
holdsAnswer: true,
requiredEvidenceSignal: "filed-gstr2b-not-generated",
}),
"download-unconfirmed": base({}),
blocked: base({}),
failed: base({}),
cancelled: base({}),
};

/** Every question answers "no" unless a status says otherwise, so a new member starts inert. */
function base(
overrides: Partial<FiledReturnsTargetStatusBehaviour>,
): FiledReturnsTargetStatusBehaviour {
return {
active: false,
resolved: false,
statedAbsence: false,
producedFile: false,
holdsAnswer: false,
requiredEvidenceSignal: null,
...overrides,
};
}

export function filedReturnsTargetStatusBehaviour(
status: FiledReturnsFullFiscalYearTargetStatus,
): FiledReturnsTargetStatusBehaviour {
return TARGET_STATUS_BEHAVIOUR[status];
}

export function isResolvedFullFiscalYearTargetStatus(
status: FiledReturnsFullFiscalYearTargetStatus,
): boolean {
return RESOLVED_TARGET_STATUSES.has(status);
return TARGET_STATUS_BEHAVIOUR[status].resolved;
}

/** The portal said there is no artifact here -- whoever it made the claim about. */
export function statesFullFiscalYearTargetAbsence(
status: FiledReturnsFullFiscalYearTargetStatus,
): boolean {
return TARGET_STATUS_BEHAVIOUR[status].statedAbsence;
}

/** An answer a run must not discard or overwrite, whether the portal or a person gave it. */
export function holdsFullFiscalYearTargetAnswer(
status: FiledReturnsFullFiscalYearTargetStatus,
): boolean {
return TARGET_STATUS_BEHAVIOUR[status].holdsAnswer;
}

/**
* Unresolved, and not a state the run reaches by itself. What is left needs the user to choose.
*
* Derived rather than listed: a status that is neither resolved nor pending/running belongs here by
* Derived rather than listed: a status that is neither resolved nor active belongs here by
* definition, so a new one cannot land in neither bucket.
*/
export function needsExplicitFullFiscalYearRetry(
status: FiledReturnsFullFiscalYearTargetStatus,
): boolean {
return (
!isResolvedFullFiscalYearTargetStatus(status) && status !== "pending" && status !== "running"
);
const behaviour = TARGET_STATUS_BEHAVIOUR[status];
return !behaviour.resolved && !behaviour.active;
}

export interface FiledReturnsFullFiscalYearTarget {
Expand Down
Loading
Loading