Skip to content
Merged
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
10 changes: 6 additions & 4 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -114,9 +114,10 @@ value this build cannot render without changing it, the run keeps the tidy CSV
and reports that the workbook is unavailable. A GSTR-1 run adds only the tidy
CSV. No run emits a blank or mislabelled GSTR-3B workbook, and the former
standalone context CSV is not emitted. The data CSV has the
fixed columns `period`, `return_type`, `artifact`, `outcome`, `field_label`,
`field_path`, `value_text`, and `value_number`, with one row per period and
flattened field. Periods and artifacts without parseable JSON receive fixed
fixed columns `financial_year`, `period`, `return_type`, `artifact`, `outcome`,
`field_label`, `field_path`, `value_text`, and `value_number`; the selected
financial year is repeated in every row so an extracted CSV identifies its
scope. Periods and artifacts without parseable JSON receive fixed
outcome rows instead of fabricated zeroes. The exact shaping rules are recorded
below for the producing Pack version.

Expand Down Expand Up @@ -160,7 +161,8 @@ are not assigned to any released Pack version because no release contains this
format yet. The producing Pack version is available in the installed extension
manifest. Neither file carries an in-file format marker, so a machine consumer
cannot identify the CSV format from the CSV alone and must be given the
producing Pack version.
producing Pack version. Its `financial_year` column nevertheless identifies the
selected year in every row.

- **Envelope rule:** Pack classifies identity against the whole JSON document,
then removes the artifact validator's documented return envelope before
Expand Down
8 changes: 5 additions & 3 deletions docs/PRIVACY_QA.md
Original file line number Diff line number Diff line change
Expand Up @@ -83,8 +83,9 @@ For each release candidate:
- No assembly may emit a GSTR-3B workbook for another return type. The
standalone context CSV must be absent.
The data CSV must keep
the fixed tidy columns `period`, `return_type`, `artifact`, `outcome`,
`field_label`, `field_path`, `value_text`, and `value_number`; keep canonical
the fixed tidy columns `financial_year`, `period`, `return_type`, `artifact`,
`outcome`, `field_label`, `field_path`, `value_text`, and `value_number`;
repeat the selected financial year in every row and keep canonical
JSON Pointer paths. Confirm only the configured GSTR-3B summary arrays with
at most 64 elements may expand, using the first shared discriminator in the
ordered candidate list `ty`, `pos`, and only when every discriminator is
Expand Down Expand Up @@ -146,7 +147,8 @@ For each release candidate:
the README, not in generated rule rows or a second sheet. The workbook and
CSV have no in-file format marker; a machine consumer of a separated CSV must
be given the producing Pack version because it cannot infer that version from
the CSV alone.
the CSV alone; the `financial_year` column still identifies the selected year
in every row.
Both derived files must remain
output-only ZIP entries: their bytes
may be transient in extension-controlled memory before browser handoff and
Expand Down
24 changes: 17 additions & 7 deletions src/connectors/gst/filed-returns-summary-sheet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,7 @@ export const MAX_FILED_RETURNS_SUMMARY_ROWS = 100_000;
export const MAX_FILED_RETURNS_SUMMARY_ARRAY_EXPANSION_ELEMENTS = 64;

export const FILED_RETURNS_SUMMARY_HEADERS = [
"financial_year",
"period",
"return_type",
"artifact",
Expand Down Expand Up @@ -82,6 +83,7 @@ export interface FiledReturnsSummaryDataRow {
artifact: FiledReturnsConcreteArtifactType;
fieldLabel: string;
fieldPath: string;
financialYear: string;
outcome: string;
period: FiledReturnsMonth;
returnType: FiledReturnsReturnType;
Expand Down Expand Up @@ -151,6 +153,7 @@ export function buildFiledReturnsSummarySheet(
maxOutputBytes = Number.POSITIVE_INFINITY,
): FiledReturnsSummarySheet {
const sortedPlan = [...plan].sort(comparePlanEntries);
const financialYear = summaryFinancialYear(sortedPlan);
const entriesByPath = new Map(entries.map((entry) => [entry.path, entry]));
const parsedPeriods = new Set<FiledReturnsMonth>();
let remainingFlattenedBytes = maxOutputBytes;
Expand Down Expand Up @@ -212,11 +215,12 @@ export function buildFiledReturnsSummarySheet(
!(leaf.valueKind === "text" && isCredentialShapedValue(leaf.value)),
);
if (fieldLeaves.length === 0) {
dataRows.push(outcomeRow(parsed.planned, parsed.outcome));
dataRows.push(outcomeRow(parsed.planned, financialYear, parsed.outcome));
continue;
}
for (const leaf of fieldLeaves) {
dataRows.push({
financialYear,
period: parsed.planned.period,
returnType: parsed.planned.returnType,
artifact: parsed.planned.artifactType,
Expand All @@ -240,7 +244,7 @@ export function buildFiledReturnsSummarySheet(
maxUtf8Bytes: maxOutputBytes,
});
const dataBytes = new TextEncoder().encode(dataCsv);
const contextRows = buildContextRows(sortedPlan, identities);
const contextRows = buildContextRows(identities);
return {
contextRows,
dataBytes,
Expand Down Expand Up @@ -487,9 +491,11 @@ export function isValidGstin(value: string): boolean {

function outcomeRow(
planned: FiledReturnsSummaryPlanEntry,
financialYear: string,
outcome: string,
): FiledReturnsSummaryDataRow {
return {
financialYear,
period: planned.period,
returnType: planned.returnType,
artifact: planned.artifactType,
Expand All @@ -500,13 +506,8 @@ function outcomeRow(
}

function buildContextRows(
plan: readonly FiledReturnsSummaryPlanEntry[],
identityValues: readonly SummaryIdentityValue[],
): FiledReturnsSummaryContextRow[] {
const financialYears = sortedUnique(plan.map((entry) => entry.financialYear));
if (financialYears.length !== 1) {
throw new SyntaxError("Filed-return summary plan must have one financial year.");
}
const identities = [...identityValues]
.sort(
(left, right) =>
Expand All @@ -524,8 +525,17 @@ function buildContextRows(
return identities;
}

function summaryFinancialYear(plan: readonly FiledReturnsSummaryPlanEntry[]): string {
const financialYears = sortedUnique(plan.map((entry) => entry.financialYear));
if (financialYears.length !== 1) {
throw new SyntaxError("Filed-return summary plan must have one financial year.");
}
return financialYears[0]!;
}

function dataCsvRow(row: FiledReturnsSummaryDataRow): Record<string, CsvCellValue> {
return {
financial_year: row.financialYear,
period: row.period,
return_type: row.returnType,
artifact: row.artifact,
Expand Down
25 changes: 25 additions & 0 deletions tests/connectors/filed-returns-summary-sheet.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,31 @@ const syntheticJwt = [
].join(".");

describe("filed-return full-year summary sheet", () => {
it("emits the single planned financial year in every CSV row", () => {
const summary = buildFiledReturnsSummarySheet(
[jsonPlan("April", "april-data.json", "GSTR-3B")],
[jsonEntry("april-data.json", "GSTR-3B", { sup_details: { osup_det: { txval: 12.5 } } })],
);

expect(new TextDecoder().decode(summary.dataBytes)).toBe(
"financial_year,period,return_type,artifact,outcome,field_label,field_path,value_text,value_number\n" +
"2026-27,April,GSTR-3B,JSON,parseable-json,,/ret_period,042026,\n" +
'2026-27,April,GSTR-3B,JSON,parseable-json,"Table 3.1(a) Outward taxable supplies (other than zero rated, nil rated and exempted) — Taxable value",/sup_details/osup_det/txval,,12.5\n',
);
});

it("refuses a mixed-year plan before it can form a CSV", () => {
expect(() =>
buildFiledReturnsSummarySheet(
[
jsonPlan("April", "april-data.json", "GSTR-3B"),
{ ...jsonPlan("May", "may-data.json", "GSTR-3B"), financialYear: "2027-28" },
],
[],
),
).toThrow("Filed-return summary plan must have one financial year.");
});

it("refuses an identity-shaped array discriminator instead of embedding it in a path", () => {
const build = (ty: string) =>
buildFiledReturnsSummarySheet(
Expand Down
10 changes: 5 additions & 5 deletions tests/entrypoints/offscreen.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -597,10 +597,10 @@ describe("offscreen Blob URL entrypoint", () => {
]);
const summary = new TextDecoder().decode(entries.get("full-year-summary.csv"));
expect(summary.split("\n")[0]).toBe(
"period,return_type,artifact,outcome,field_label,field_path,value_text,value_number",
"financial_year,period,return_type,artifact,outcome,field_label,field_path,value_text,value_number",
);
expect(summary).toContain("April,GSTR-3B,JSON,parseable-json,,/portal_leaf,,11");
expect(summary).toContain("May,GSTR-3B,JSON,parseable-json,,/other_portal_leaf,,22");
expect(summary).toContain("2026-27,April,GSTR-3B,JSON,parseable-json,,/portal_leaf,,11");
expect(summary).toContain("2026-27,May,GSTR-3B,JSON,parseable-json,,/other_portal_leaf,,22");
expect(summary).not.toContain("900");
expect(summary).not.toContain("800");
expect(summary).not.toContain("700");
Expand Down Expand Up @@ -712,7 +712,7 @@ describe("offscreen Blob URL entrypoint", () => {
});
const entries = await extractStoredZipEntries(createdBlobs[0]!);
expect(new TextDecoder().decode(entries.get("full-year-summary.csv"))).toContain(
"April,GSTR-3B,PDF,non-json-artifact",
"2026-27,April,GSTR-3B,PDF,non-json-artifact",
);
});

Expand Down Expand Up @@ -760,7 +760,7 @@ describe("offscreen Blob URL entrypoint", () => {
const entries = await extractStoredZipEntries(createdBlobs[0]!);
expect([...entries.keys()]).toEqual(["april-summary.pdf", "full-year-summary.csv"]);
expect(new TextDecoder().decode(entries.get("full-year-summary.csv"))).toContain(
"April,GSTR-1,PDF,non-json-artifact",
"2026-27,April,GSTR-1,PDF,non-json-artifact",
);
});

Expand Down