Skip to content

Commit e049872

Browse files
fix(google-sheets): normalise header case and fix monthly COUNTIFS formula (#373)
1 parent 6b492b3 commit e049872

3 files changed

Lines changed: 56 additions & 7 deletions

File tree

packages/destination-google-sheets/__tests__/examples.test.ts

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -41,9 +41,9 @@ describe('buildExampleSections', () => {
4141
expect(s.rows).toHaveLength(6) // last 6 months
4242
// Formulas should reference customers.created (column C = index 2)
4343
expect(s.rows[0][1]).toContain("'customers'!C2:C")
44-
// Formula should use EDATE for Unix timestamp conversion
44+
// Formula should use EDATE for month boundaries and DATEVALUE for ISO string parsing
4545
expect(s.rows[0][1]).toContain('EDATE')
46-
expect(s.rows[0][1]).toContain('DATE(1970,1,1)')
46+
expect(s.rows[0][1]).toContain('DATEVALUE')
4747
})
4848

4949
it('includes payment volume section when payment_intents has status and amount', () => {

packages/destination-google-sheets/src/index.test.ts

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,8 @@ import {
1010
} from './index.js'
1111
import {
1212
applyBatch,
13+
displayToField,
14+
fieldToDisplay,
1315
MAX_CELLS_PER_SPREADSHEET,
1416
readEnumValidations,
1517
readSheet,
@@ -2051,3 +2053,48 @@ describe('enum constraints on any column', () => {
20512053
).toBeUndefined()
20522054
})
20532055
})
2056+
2057+
describe('fieldToDisplay / displayToField', () => {
2058+
it('roundtrip — lowercase snake_case fields survive display conversion', () => {
2059+
const fields = ['id', 'created_at', 'customer_id', 'object', 'invoice_item_id']
2060+
for (const f of fields) {
2061+
expect(displayToField(fieldToDisplay(f))).toBe(f)
2062+
}
2063+
})
2064+
2065+
it('fieldToDisplay — single word gets sentence case', () => {
2066+
expect(fieldToDisplay('id')).toBe('Id')
2067+
expect(fieldToDisplay('object')).toBe('Object')
2068+
})
2069+
2070+
it('fieldToDisplay — multi-word snake_case becomes sentence case with spaces', () => {
2071+
expect(fieldToDisplay('created_at')).toBe('Created at')
2072+
expect(fieldToDisplay('customer_id')).toBe('Customer id')
2073+
})
2074+
2075+
it('fieldToDisplay — uppercase in input is normalised to lowercase (no case leakage)', () => {
2076+
expect(fieldToDisplay('API_version')).toBe('Api version')
2077+
expect(fieldToDisplay('UPPER_case')).toBe('Upper case')
2078+
})
2079+
2080+
it('fieldToDisplay — system fields (leading underscore) are returned as-is', () => {
2081+
expect(fieldToDisplay('_idx')).toBe('_idx')
2082+
expect(fieldToDisplay('_updated_at')).toBe('_updated_at')
2083+
})
2084+
2085+
it('displayToField — reverses sentence case label back to snake_case', () => {
2086+
expect(displayToField('Created at')).toBe('created_at')
2087+
expect(displayToField('Customer id')).toBe('customer_id')
2088+
expect(displayToField('Id')).toBe('id')
2089+
})
2090+
2091+
it('displayToField — system fields (leading underscore) are returned as-is', () => {
2092+
expect(displayToField('_idx')).toBe('_idx')
2093+
expect(displayToField('_updated_at')).toBe('_updated_at')
2094+
})
2095+
2096+
it('displayToField — idempotent on already-snake_case strings', () => {
2097+
expect(displayToField('customer_id')).toBe('customer_id')
2098+
expect(displayToField('id')).toBe('id')
2099+
})
2100+
})

packages/destination-google-sheets/src/writer.ts

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -53,7 +53,7 @@ function colIndexToLetter(idx: number): string {
5353
/** snake_case field name → "Sentence case" display label. System fields (_x) are returned as-is. */
5454
export function fieldToDisplay(field: string): string {
5555
if (field.startsWith('_')) return field
56-
const words = field.split('_')
56+
const words = field.split('_').map((w) => w.toLowerCase())
5757
words[0] = words[0].charAt(0).toUpperCase() + words[0].slice(1)
5858
return words.join(' ')
5959
}
@@ -763,10 +763,12 @@ export function buildExampleSections(
763763
for (let i = 5; i >= 0; i--) {
764764
// EDATE shifts the first-of-month by N months; negative = past
765765
const monthLabel = `=TEXT(EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-i}),"YYYY-MM")`
766-
// Unix timestamp boundaries for each month
767-
const startUnix = `(EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-i})-DATE(1970,1,1))*86400`
768-
const endUnix = `(EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-(i - 1)})-DATE(1970,1,1))*86400`
769-
rows.push([monthLabel, `=COUNTIFS(${createdRange},">="&${startUnix},${createdRange},"<"&${endUnix})`])
766+
// COUNTIFS >=/< only works on numbers/dates, not text strings.
767+
// DATEVALUE(LEFT(cell,10)) parses the "yyyy-mm-dd" prefix of the ISO string into a Sheets date serial.
768+
const start = `EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-i})`
769+
const end = `EDATE(DATE(YEAR(TODAY()),MONTH(TODAY()),1),${-(i - 1)})`
770+
const parsed = `IFERROR(DATEVALUE(LEFT(${createdRange},10)),0)`
771+
rows.push([monthLabel, `=SUMPRODUCT((${parsed}>=${start})*(${parsed}<${end}))`])
770772
}
771773
sections.push({
772774
title: 'New Customers by Month',

0 commit comments

Comments
 (0)