diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 56b46efd2652..25fb91ed95e8 100644
--- a/pnpm-lock.yaml
+++ b/pnpm-lock.yaml
@@ -3829,9 +3829,18 @@ importers:
'@date-fns/tz':
specifier: 1.4.1
version: 1.4.1
+ '@tanstack/react-query':
+ specifier: 5.90.8
+ version: 5.90.8(react@18.3.1)
+ '@wordpress/api-fetch':
+ specifier: 7.46.0
+ version: 7.46.0
'@wordpress/boot':
specifier: 0.13.0
version: 0.13.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/core-data':
+ specifier: 7.46.0
+ version: 7.46.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
'@wordpress/data':
specifier: 10.46.0
version: 10.46.0(react@18.3.1)
@@ -3844,6 +3853,9 @@ importers:
'@wordpress/route':
specifier: 0.12.0
version: 0.12.0(react-dom@18.3.1(react@18.3.1))(react@18.3.1)
+ '@wordpress/url':
+ specifier: 4.46.0
+ version: 4.46.0
date-fns:
specifier: 4.1.0
version: 4.1.0
@@ -3854,9 +3866,18 @@ importers:
specifier: 18.3.1
version: 18.3.1(react@18.3.1)
devDependencies:
+ '@automattic/jetpack-webpack-config':
+ specifier: workspace:*
+ version: link:../../js-packages/webpack-config
'@babel/core':
specifier: 7.29.0
version: 7.29.0
+ '@tanstack/react-query-devtools':
+ specifier: 5.90.2
+ version: 5.90.2(@tanstack/react-query@5.90.8(react@18.3.1))(react@18.3.1)
+ '@types/jest':
+ specifier: 30.0.0
+ version: 30.0.0
'@typescript/native-preview':
specifier: 7.0.0-dev.20260225.1
version: 7.0.0-dev.20260225.1
@@ -3866,6 +3887,9 @@ importers:
browserslist:
specifier: 4.28.2
version: 4.28.2
+ jest:
+ specifier: 30.4.2
+ version: 30.4.2
projects/packages/protect-models: {}
diff --git a/projects/packages/premium-analytics/babel.config.cjs b/projects/packages/premium-analytics/babel.config.cjs
new file mode 100644
index 000000000000..ed6bf0680c44
--- /dev/null
+++ b/projects/packages/premium-analytics/babel.config.cjs
@@ -0,0 +1,8 @@
+module.exports = {
+ presets: [
+ [
+ '@automattic/jetpack-webpack-config/babel/preset',
+ { pluginReplaceTextdomain: { textdomain: 'jetpack-premium-analytics' } },
+ ],
+ ],
+};
diff --git a/projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics b/projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics
new file mode 100644
index 000000000000..d2029922c0f4
--- /dev/null
+++ b/projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-analytics
@@ -0,0 +1,4 @@
+Significance: patch
+Type: added
+
+Port data package (React Query report hooks, fetchers, and processing) as an internal package from next-woocommerce-analytics.
diff --git a/projects/packages/premium-analytics/eslint.config.mjs b/projects/packages/premium-analytics/eslint.config.mjs
index 4d905c2efea4..b0ef3d32d68f 100644
--- a/projects/packages/premium-analytics/eslint.config.mjs
+++ b/projects/packages/premium-analytics/eslint.config.mjs
@@ -1,16 +1,30 @@
import { makeBaseConfig, defineConfig } from 'jetpack-js-tools/eslintrc/base.mjs';
/**
- * Soften JSDoc rules for `packages/datetime/**` so the initial port can
- * land. Temporary — backfill proper descriptions on the helpers and
- * remove this override (at which point this whole file can go away).
+ * Soften JSDoc rules for the internal `packages/*` ports so the initial
+ * ports can land. Temporary — backfill proper descriptions on the helpers
+ * and remove this override (at which point this whole file can go away).
*/
-export default defineConfig( makeBaseConfig( import.meta.url ), {
- files: [ 'packages/datetime/**' ],
- rules: {
- 'jsdoc/require-description': 'off',
- 'jsdoc/require-param-description': 'off',
- 'jsdoc/require-returns': 'off',
- 'jsdoc/check-indentation': 'off',
+export default defineConfig(
+ makeBaseConfig( import.meta.url ),
+ {
+ files: [ 'packages/datetime/**', 'packages/data/**' ],
+ rules: {
+ 'jsdoc/require-description': 'off',
+ 'jsdoc/require-param-description': 'off',
+ 'jsdoc/require-returns': 'off',
+ 'jsdoc/check-indentation': 'off',
+ },
},
-} );
+ {
+ // The data port carries a couple of upstream patterns this temporary
+ // override keeps as-is: intentional `any` escapes for the generic report
+ // `TData` (see use-report.ts), and `react` flagged as extraneous because
+ // the internal package's deps are declared on the parent manifest.
+ files: [ 'packages/data/**' ],
+ rules: {
+ '@typescript-eslint/no-explicit-any': 'off',
+ 'import/no-extraneous-dependencies': 'off',
+ },
+ }
+);
diff --git a/projects/packages/premium-analytics/package.json b/projects/packages/premium-analytics/package.json
index 3f0ab771f18b..254d21c6e6f4 100644
--- a/projects/packages/premium-analytics/package.json
+++ b/projects/packages/premium-analytics/package.json
@@ -6,6 +6,7 @@
"scripts": {
"build": "wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php",
"build-production": "NODE_ENV=production wp-build && mkdir -p build/modules/boot && cp shims/boot-asset.php build/modules/boot/index.min.asset.php",
+ "test": "jest --config=tests/jest.config.cjs",
"typecheck": "tsgo --noEmit",
"watch": "wp-build --watch"
},
@@ -30,19 +31,27 @@
},
"dependencies": {
"@date-fns/tz": "1.4.1",
+ "@tanstack/react-query": "5.90.8",
+ "@wordpress/api-fetch": "7.46.0",
"@wordpress/boot": "0.13.0",
+ "@wordpress/core-data": "7.46.0",
"@wordpress/data": "10.46.0",
"@wordpress/i18n": "^6.9.0",
"@wordpress/icons": "^13.0.0",
"@wordpress/route": "0.12.0",
+ "@wordpress/url": "4.46.0",
"date-fns": "4.1.0",
"react": "18.3.1",
"react-dom": "18.3.1"
},
"devDependencies": {
+ "@automattic/jetpack-webpack-config": "workspace:*",
"@babel/core": "7.29.0",
+ "@tanstack/react-query-devtools": "5.90.2",
+ "@types/jest": "30.0.0",
"@typescript/native-preview": "7.0.0-dev.20260225.1",
"@wordpress/build": "0.14.0",
- "browserslist": "4.28.2"
+ "browserslist": "4.28.2",
+ "jest": "30.4.2"
}
}
diff --git a/projects/packages/premium-analytics/packages/data/README.md b/projects/packages/premium-analytics/packages/data/README.md
new file mode 100644
index 000000000000..c8492a081340
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/README.md
@@ -0,0 +1,431 @@
+# @automattic/jetpack-premium-analytics-data
+
+Data management for Jetpack Premium Analytics with React Query integration.
+
+## Installation
+
+This is an internal package of Jetpack Premium Analytics — it is never
+published to npm and is resolved entirely in-tree. It's automatically
+available to routes and other internal packages within
+`@automattic/jetpack-premium-analytics`.
+
+```tsx
+import {
+ AnalyticsQueryClientProvider,
+ useReport,
+ prefetchReport,
+ // ... other exports
+} from '@automattic/jetpack-premium-analytics-data';
+```
+
+## Features
+
+- **React Query Integration**: Built on `@tanstack/react-query` for
+ caching and state management
+- **Prefetching Support**: Route-based data prefetching for improved
+ performance
+- **Data Processing**: Automatic sanitization of API responses
+ (strings → numbers)
+- **Comparison Support**: Built-in primary + comparison data fetching
+- **TypeScript Support**: Fully typed data structures and API responses
+- **Smart Caching**: Automatic cache invalidation and background
+ refetching
+
+## Usage
+
+### Setup
+
+```tsx
+import { AnalyticsQueryClientProvider } from '@automattic/jetpack-premium-analytics-data';
+
+function App() {
+ return (
+
+ {/* Your app components */}
+
+ );
+}
+```
+
+### Fetching Data
+
+```tsx
+import {
+ useReportOrders,
+ useReportOrdersByProductType,
+ useReportOrderAttribution,
+ useReportCoupons
+} from '@automattic/jetpack-premium-analytics-data';
+
+function OrdersReport() {
+ // Orders endpoint separates primary and comparison periods
+ const { primary, comparison, hasComparison } = useReportOrders( {
+ from: '2025-07-01T00:00:00',
+ to: '2025-07-29T23:59:59',
+ interval: 'day',
+ } );
+}
+
+function OrdersByProductTypeReport() {
+ // Orders by product type with filtering support
+ const { primary, comparison, hasComparison } = useReportOrdersByProductType( {
+ from: '2025-07-01T00:00:00',
+ to: '2025-07-29T23:59:59',
+ interval: 'day',
+ filters: [
+ {
+ key: 'product_type',
+ value: ['simple'],
+ compare: 'IN'
+ }
+ ]
+ } );
+}
+
+function OrderAttributionReport() {
+ // Order attribution requires a view parameter
+ const { primary, comparison, hasComparison } = useReportOrderAttribution( {
+ from: '2025-07-01T00:00:00',
+ to: '2025-07-29T23:59:59',
+ interval: 'day',
+ view: 'channel', // 'channel' | 'source' | 'campaign' | 'device' | 'channel-source'
+ } );
+}
+
+function CouponsReport() {
+ // Coupons works like orders with separate comparison requests
+ const { primary, comparison, hasComparison } = useReportCoupons( {
+ from: '2025-07-01T00:00:00',
+ to: '2025-07-29T23:59:59',
+ interval: 'day',
+ } );
+}
+```
+
+### Prefetching
+
+```tsx
+import { prefetchReport, ensureCoreSettingsReady } from '@automattic/jetpack-premium-analytics-data';
+
+export const route = {
+ beforeLoad: async () => {
+ // Ensure site settings are loaded first
+ await ensureCoreSettingsReady();
+
+ // Now safely prefetch reports
+ await prefetchReport( 'orders' );
+ },
+};
+```
+
+## API Reference
+
+### Individual Hooks (Recommended)
+
+#### `useReportOrders( params )`
+
+Fetches orders report data with automatic processing and comparison support.
+
+**Parameters:**
+- `params`: `ReportParams` with `from`, `to`, `interval`, and optional comparison params
+
+**Returns:** `{ primary, comparison, hasComparison }`
+
+#### `useReportOrdersByProductType( params )`
+
+Fetches orders report data filtered by product type or other product characteristics with automatic processing and comparison support.
+
+**Parameters:**
+- `params`: `ReportParams` with `from`, `to`, `interval`, optional `filters` array, and optional comparison params
+
+**Filters Structure:**
+```typescript
+filters: Array<{
+ key: string; // e.g., 'product_type', 'virtual'
+ value: string | string[]; // e.g., ['simple'], '1'
+ compare: '=' | 'IN' | 'NOT IN' | '!=' | '>' | '<' | '>=' | '<=';
+}>
+```
+
+**Common Filter Examples:**
+- Product types: `{ key: 'product_type', value: ['simple', 'variable'], compare: 'IN' }`
+- Virtual products: `{ key: 'virtual', value: '1', compare: '=' }`
+- Non-virtual products: `{ key: 'virtual', value: '0', compare: '=' }`
+
+**Returns:** `{ primary, comparison, hasComparison }`
+
+#### `useReportOrderAttribution( params )`
+
+Fetches order attribution data with built-in comparison handling.
+
+**Parameters:**
+- `params`: `ReportParams` with `from`, `to`, `interval`, `view`, and optional comparison params
+
+**Returns:** `{ primary, comparison, hasComparison }`
+
+#### `useReportCoupons( params )`
+
+Fetches coupons report data with automatic processing and comparison support.
+
+**Parameters:**
+- `params`: `ReportParams` with `from`, `to`, `interval`, and optional comparison params
+
+**Returns:** `{ primary, comparison, hasComparison }`
+
+### Legacy Hook (Deprecated)
+
+#### `useReport( reportType, params )`
+
+**⚠️ Deprecated:** Use individual hooks instead for better type safety and performance.
+
+**Parameters:**
+- `reportType`: `'orders'` | `'orders-by-product-type'` | `'order-attribution'` | `'coupons'`
+- `params`: `ReportParams`
+
+**Returns:** Same as individual hooks above
+
+### `prefetchReport( reportType, params )`
+
+Prefetches data for improved performance. Same parameters as `useReport`.
+
+**Usage:** Call in route `beforeLoad` functions for instant data loading
+
+**Returns:** Promise that resolves when data is prefetched
+
+**Caching:** Uses React Query cache, so subsequent `useReport` calls are
+instant
+
+**Example:**
+```tsx
+// Prefetch orders data
+await prefetchReport( 'orders', { from, to, interval } );
+
+// Prefetch orders by product type data
+await prefetchReport( 'orders-by-product-type', {
+ from,
+ to,
+ interval,
+ filters: [{ key: 'product_type', value: ['simple'], compare: 'IN' }]
+} );
+
+// Prefetch order attribution data
+await prefetchReport( 'order-attribution', { from, to, interval, view: 'channel' } );
+
+// Prefetch coupons data
+await prefetchReport( 'coupons', { from, to, interval } );
+```
+
+### `normalizeReportParams( params? )`
+
+Normalizes and validates report parameters, providing defaults when needed.
+
+**Parameters:**
+- `params`: Optional partial parameters object
+
+**Returns:** `{ primary, comparison? }` with normalized parameters
+
+**Defaults:** Last 30 days, daily interval when not specified
+
+**Validation:** Ensures required fields are present for API calls
+
+### `getDefaultIntervalForPeriod( period, from, to )`
+
+Returns the optimal default interval for a given time period.
+
+**Parameters:**
+- `period`: `string` - Period identifier (e.g., 'today', 'last-7-days', 'last-30-days')
+- `from`: `string` - Start date
+- `to`: `string` - End date
+
+**Returns:** `IntervalType` - Optimal interval ('hour', 'day', 'week', 'month', 'quarter', 'year')
+
+**Example:**
+```tsx
+import { getDefaultIntervalForPeriod } from '@automattic/jetpack-premium-analytics-data';
+
+const interval = getDefaultIntervalForPeriod( 'last-7-days', from, to ); // Returns 'day'
+```
+
+### `ORDER_ATTRIBUTION_VIEWS`
+
+Constant array of available order attribution views.
+
+**Values:** `['channel', 'source', 'campaign', 'device', 'channel-source']`
+
+**Example:**
+```tsx
+import { ORDER_ATTRIBUTION_VIEWS } from '@automattic/jetpack-premium-analytics-data';
+
+// Use in components for view selection
+const views = ORDER_ATTRIBUTION_VIEWS; // ['channel', 'source', ...]
+```
+
+## Architecture
+
+```
+src/
+├── api/ # API functions and query keys
+│ ├── index.ts # API exports
+│ ├── constants.ts # Shared API endpoint constants
+│ ├── report-orders-fetch/ # Orders API client
+│ │ ├── index.ts # Orders API exports
+│ │ └── report-orders-fetch.ts # Orders API implementation
+│ ├── report-orders-by-product-type-fetch/ # Orders by product type API client
+│ │ ├── index.ts # Orders by product type API exports
+│ │ └── report-orders-by-product-type-fetch.ts # Orders by product type API implementation
+│ ├── report-order-attribution-summary-fetch/ # Attribution API client
+│ │ ├── index.ts # Attribution API exports
+│ │ └── report-order-attribution-summary-fetch.ts # Attribution API implementation
+│ └── report-coupons-fetch/ # Coupons API client
+│ ├── index.ts # Coupons API exports
+│ └── report-coupons-fetch.ts # Coupons API implementation
+├── queries/ # React Query configurations
+│ ├── index.ts # Query exports
+│ ├── report-orders-query.ts # Orders query definition
+│ ├── report-orders-by-product-type-query.ts # Orders by product type query definition
+│ ├── report-order-attribution-summary-query.ts # Attribution query definition
+│ └── report-coupons-query.ts # Coupons query definition
+├── hooks/ # React hooks
+│ ├── index.ts # Hook exports
+│ └── use-report.ts # Main useReport hook with comparison
+├── prefetch/ # Prefetching functions
+│ ├── index.ts # Prefetch exports
+│ └── prefetch-report-orders.ts # Multi-report prefetch logic (orders + attribution + coupons)
+├── processing/ # Data sanitization and transformation
+│ ├── orders/ # Orders-specific data processing
+│ ├── orders-by-product-type/ # Orders by product type data processing
+│ ├── coupons/ # Coupons data processing
+│ └── order-attribution/ # Attribution data processing
+├── providers/ # React Context providers
+│ ├── index.ts # Provider exports
+│ └── query-client-provider.tsx # React Query client setup
+├── defaults/ # Default parameters and configurations
+├── utils/ # Utility functions
+│ ├── date.ts # Date manipulation utilities (timezone-aware)
+│ ├── ensure-core-settings.ts # Core settings initialization
+│ ├── interval.ts # Interval calculation and optimization
+│ ├── search.ts # Search parameter utilities
+│ └── types.ts # Shared utility types (Override, etc.)
+└── types.ts # TypeScript type definitions
+```
+
+### Data Flow
+
+1. **Route Prefetching**: `beforeLoad` calls `prefetchReport()` to load
+ data
+2. **Component Consumption**: Components use `useReport()` to access cached
+ data
+3. **Automatic Processing**: Raw API responses are sanitized
+ (strings → numbers)
+4. **Comparison Handling**: Primary and comparison queries are managed
+ automatically
+5. **Cache Management**: React Query handles caching, background updates,
+ and invalidation
+
+## Date Utilities
+
+This package provides timezone-aware date utilities that integrate with
+WordPress site settings:
+
+### `localTZDate( value?, timezone? )`
+
+Creates a timezone-aware date using the site's configured timezone by
+default.
+
+```typescript
+import { localTZDate } from '@automattic/jetpack-premium-analytics-data';
+
+const now = localTZDate(); // Current time in site timezone
+const custom = localTZDate( '2024-01-15', 'America/New_York' );
+```
+
+**Parameters:**
+- `value` (optional): `number | string | Date` - Date value to convert
+- `timezone` (optional): `string` - Target timezone (defaults to site
+ timezone)
+
+**Returns:** `TZDate` - Timezone-aware date object
+
+
+### `dateToISOStringWithLocalTZ( date, timezone? )`
+
+Converts a date to ISO string with the site's timezone offset applied.
+
+```typescript
+const withTZ = dateToISOStringWithLocalTZ( new Date() );
+// Returns: "2024-01-15T14:30:00.000-05:00" (with site timezone offset)
+```
+
+**Parameters:**
+- `date`: `Date` - Date to convert
+- `timezone` (optional): `string` - Target timezone (defaults to site
+ timezone)
+
+**Returns:** `string` - ISO string with timezone offset
+
+### `getSiteTimezone()`
+
+Returns the WordPress site's configured timezone string.
+
+```typescript
+const timezone = getSiteTimezone();
+// Returns: "America/New_York" or "+05:30" (offset format)
+```
+
+**Returns:** `string` - Site timezone from WordPress settings
+
+**Note:** This function will throw an error if called before core settings
+are loaded. Use `ensureCoreSettingsReady()` in route loaders to prevent
+this.
+
+### `ensureCoreSettingsReady()`
+
+Ensures WordPress core settings (site and general settings) are loaded
+before accessing timezone-dependent functions.
+
+```typescript
+// In route loaders or beforeLoad hooks
+await ensureCoreSettingsReady();
+// Now getSiteTimezone() can be safely called
+```
+
+**Returns:** `Promise` - Resolves when settings are loaded
+
+**Features:**
+- Memoizes the promise to avoid duplicate requests
+- Prevents race conditions during navigation
+- Essential for route prefetching and hover preloading
+
+These functions automatically use the WordPress site's timezone settings
+and provide consistent date handling across the analytics interface.
+
+## Complete API Exports
+
+This package exports the following public API:
+
+### Components
+- `AnalyticsQueryClientProvider` - React Query provider wrapper
+
+### Hooks
+- `useReportOrders` - Hook for fetching orders report data
+- `useReportOrdersByProductType` - Hook for fetching orders by product type with filtering
+- `useReportOrderAttribution` - Hook for fetching order attribution data
+- `useReportCoupons` - Hook for fetching coupons report data
+- `useReport` - Legacy main hook for fetching report data (deprecated)
+
+### Functions
+- `prefetchReport` - Prefetch data for routes
+- `normalizeReportParams` - Normalize and validate parameters
+- `getDefaultIntervalForPeriod` - Get optimal interval for time period
+
+### Date Utilities
+- `localTZDate` - Create timezone-aware dates
+- `dateToISOStringWithLocalTZ` - Convert to ISO with timezone
+- `getSiteTimezone` - Get WordPress site timezone
+- `ensureCoreSettingsReady` - Ensure settings are loaded
+
+### Constants
+- `ORDER_ATTRIBUTION_VIEWS` - Available attribution view types
+
+### Types
+- `ReportDataMap` - TypeScript type mapping for report data structures
diff --git a/projects/packages/premium-analytics/packages/data/package.json b/projects/packages/premium-analytics/packages/data/package.json
new file mode 100644
index 000000000000..74470af6fbd8
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/package.json
@@ -0,0 +1,23 @@
+{
+ "name": "@automattic/jetpack-premium-analytics-data",
+ "version": "0.1.0",
+ "private": true,
+ "type": "module",
+ "main": "src/index.ts",
+ "types": "src/index.ts",
+ "sideEffects": false,
+ "dependencies": {
+ "@date-fns/tz": "1.4.1",
+ "@jetpack-premium-analytics/datetime": "workspace:*",
+ "@tanstack/react-query": "5.90.8",
+ "@wordpress/api-fetch": "7.46.0",
+ "@wordpress/core-data": "7.46.0",
+ "@wordpress/data": "10.46.0",
+ "@wordpress/i18n": "^6.9.0",
+ "@wordpress/url": "4.46.0",
+ "date-fns": "4.1.0"
+ },
+ "devDependencies": {
+ "@tanstack/react-query-devtools": "5.90.2"
+ }
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/constants.ts b/projects/packages/premium-analytics/packages/data/src/api/constants.ts
new file mode 100644
index 000000000000..eb04e41b4633
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/constants.ts
@@ -0,0 +1,4 @@
+/**
+ * Constants for API endpoints
+ */
+export const reportsPath = '/wc/v3/woocommerce-analytics/proxy/reports';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/index.ts b/projects/packages/premium-analytics/packages/data/src/api/index.ts
new file mode 100644
index 000000000000..771d51ddba7b
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts
@@ -0,0 +1,45 @@
+/**
+ * Internal dependencies
+ */
+import type { RequestReportBookingsParams } from './report-bookings-fetch';
+import type { RequestReportCouponsByDateParams } from './report-coupons-by-date-fetch';
+import type { RequestReportCouponsParams } from './report-coupons-fetch';
+import type { RequestReportCustomersParams } from './report-customers-fetch';
+import type { RequestReportOrderAttributionByProductParams } from './report-order-attribution-by-product-fetch';
+import type { RequestReportOrderAttributionSummaryParams } from './report-order-attribution-summary-fetch';
+import type { RequestReportOrdersParams } from './report-orders-fetch';
+import type { RequestReportProductsParams } from './report-products-fetch';
+import type { RequestReportSessionsByDeviceParams } from './report-sessions-by-device-fetch';
+import type { RequestReportVisitorsByLocationParams } from './report-visitors-by-location-fetch';
+import type { RequestReportVisitorsParams } from './report-visitors-fetch';
+
+export type ReportQueryParams = Partial<
+ RequestReportOrdersParams &
+ RequestReportOrderAttributionSummaryParams &
+ RequestReportOrderAttributionByProductParams &
+ RequestReportCouponsParams &
+ RequestReportCouponsByDateParams &
+ RequestReportCustomersParams &
+ RequestReportProductsParams &
+ RequestReportVisitorsParams &
+ RequestReportVisitorsByLocationParams &
+ RequestReportBookingsParams &
+ RequestReportSessionsByDeviceParams
+>;
+
+export { fetchReportOrders } from './report-orders-fetch';
+export {
+ fetchReportOrderAttributionSummary,
+ ORDER_ATTRIBUTION_VIEWS,
+} from './report-order-attribution-summary-fetch';
+export { fetchReportOrderAttributionByProduct } from './report-order-attribution-by-product-fetch';
+export { fetchReportCoupons } from './report-coupons-fetch';
+export { fetchReportCouponsByDate } from './report-coupons-by-date-fetch';
+export { fetchReportCustomers } from './report-customers-fetch';
+export { fetchReportProducts } from './report-products-fetch';
+export { fetchReportVisitors } from './report-visitors-fetch';
+export { fetchReportVisitorsByLocation } from './report-visitors-by-location-fetch';
+export { fetchReportBookings } from './report-bookings-fetch';
+export { fetchReportSessionsByDevice } from './report-sessions-by-device-fetch';
+export { exportReport } from './report-export-fetch';
+export type { ExportReportParams, ExportReportResponse } from './report-export-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts
new file mode 100644
index 000000000000..8dd1607b5e9e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts
@@ -0,0 +1,5 @@
+export { fetchReportBookings } from './report-bookings-fetch';
+export type {
+ ReportsBookingsByDateResponse,
+ RequestReportBookingsParams,
+} from './report-bookings-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts
new file mode 100644
index 000000000000..c06c8fa36fa2
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts
@@ -0,0 +1,67 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+
+type ReportsBookingsByDateSummary = {
+ status_unpaid: string;
+ status_pending_confirmation: string;
+ status_confirmed: string;
+ status_paid: string;
+ status_cancelled: string;
+ status_complete: string;
+ attendance_status_booked: string;
+ attendance_status_no_show: string;
+ attendance_status_checked_in: string;
+ date_start: string;
+ date_end: string;
+};
+
+type BookingsReportDataItem = ReportsBookingsByDateSummary & {
+ time_interval: string;
+};
+
+export type ReportsBookingsByDateResponse = {
+ data: BookingsReportDataItem[];
+ summary: ReportsBookingsByDateSummary;
+};
+
+export type RequestReportBookingsParams = BaseReportParams & {
+ filters?: FilterCondition[];
+};
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.filters
+ * @param root0.date_type
+ */
+export async function fetchReportBookings( {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+}: RequestReportBookingsParams ): Promise< ReportsBookingsByDateResponse > {
+ const apiUrl = `${ reportsPath }/bookings/by-date`;
+
+ const path = addQueryArgs( apiUrl, {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+ } );
+
+ return apiFetch( { path } ) as Promise< ReportsBookingsByDateResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts
new file mode 100644
index 000000000000..f9cc046877b8
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts
@@ -0,0 +1,4 @@
+export {
+ fetchReportConversionRate,
+ type RequestReportConversionRateParams,
+} from './report-conversion-rate-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts
new file mode 100644
index 000000000000..eaea267f5bdc
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts
@@ -0,0 +1,66 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+
+type ReportsConversionRateByDateSummary = {
+ active_sessions: string;
+ visitors: string;
+ with_cart_addition: string;
+ reached_checkout: string;
+ completed_checkout: string;
+ date_end: string;
+ date_start: string;
+};
+
+type ConversionRateReportDataItem = {
+ date_start: string;
+ date_end: string;
+ active_sessions: string;
+ visitors: string;
+ with_cart_addition: string;
+ reached_checkout: string;
+ completed_checkout: string;
+};
+
+type ReportsConversionRateByDateResponse = {
+ data: ConversionRateReportDataItem[];
+ summary: ReportsConversionRateByDateSummary;
+};
+
+export type RequestReportConversionRateParams = BaseReportParams & {
+ filters?: FilterCondition[];
+};
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.filters
+ */
+export async function fetchReportConversionRate( {
+ from,
+ to,
+ interval,
+ filters,
+}: RequestReportConversionRateParams ): Promise< ReportsConversionRateByDateResponse > {
+ const path = addQueryArgs( `${ reportsPath }/sessions/by-conversion-rate`, {
+ from,
+ to,
+ interval,
+ filters,
+ } );
+
+ return apiFetch( {
+ path,
+ } ) as Promise< ReportsConversionRateByDateResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts
new file mode 100644
index 000000000000..cd9dbe298e36
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts
@@ -0,0 +1,4 @@
+export {
+ fetchReportCouponsByDate,
+ type RequestReportCouponsByDateParams,
+} from './report-coupons-by-date-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts
new file mode 100644
index 000000000000..d58602ec6645
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+
+type CouponsByDateDataItem = {
+ time_interval: string;
+ date_start: string;
+ date_end: string;
+ total_orders: string;
+ orders_with_coupon: string;
+ orders_without_coupon: string;
+ total_sales: string;
+ sales_with_coupon: string;
+ sales_without_coupon: string;
+ total_discount_amount: string;
+ net_sales_after_discount: string;
+ coupon_usage_percentage: string;
+};
+
+type CouponsByDateSummary = {
+ total_orders: string;
+ orders_with_coupon: string;
+ orders_without_coupon: string;
+ total_sales: string;
+ sales_with_coupon: string;
+ sales_without_coupon: string;
+ total_discount_amount: string;
+ net_sales_after_discount: string;
+ coupon_usage_percentage: string;
+ date_start: string;
+ date_end: string;
+};
+
+export type ReportsCouponsByDateResponse = {
+ summary: CouponsByDateSummary;
+ data: CouponsByDateDataItem[];
+};
+
+export type RequestReportCouponsByDateParams = BaseReportParams & {
+ filters?: FilterCondition[];
+};
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.filters
+ * @param root0.date_type
+ */
+export async function fetchReportCouponsByDate( {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+}: RequestReportCouponsByDateParams ): Promise< ReportsCouponsByDateResponse > {
+ const path = addQueryArgs( `${ reportsPath }/coupons/by-date`, {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+ } );
+
+ return apiFetch< ReportsCouponsByDateResponse >( { path } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts
new file mode 100644
index 000000000000..54bbee440ef2
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts
@@ -0,0 +1 @@
+export { fetchReportCoupons, type RequestReportCouponsParams } from './report-coupons-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts
new file mode 100644
index 000000000000..c25dc9dd29c0
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts
@@ -0,0 +1,63 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+
+type CouponsDataItem = {
+ coupon_code: string;
+ discount_amount: string;
+ total_sales: string;
+ orders_count: string;
+};
+
+type CouponsDataSummary = {
+ total_sales: string;
+ total_discount_amount: string;
+ total_orders: string;
+ date_start: string;
+ date_end: string;
+};
+
+export type ReportsCouponsResponse = {
+ summary: CouponsDataSummary;
+ data: CouponsDataItem[];
+};
+
+export type RequestReportCouponsParams = BaseReportParams & {
+ filters?: FilterCondition[];
+};
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.filters
+ * @param root0.date_type
+ */
+export async function fetchReportCoupons( {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+}: RequestReportCouponsParams ): Promise< ReportsCouponsResponse > {
+ const path = addQueryArgs( `${ reportsPath }/coupons/`, {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+ orderby: 'total_sales',
+ } );
+
+ return apiFetch< ReportsCouponsResponse >( { path } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts
new file mode 100644
index 000000000000..ec5993677f34
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts
@@ -0,0 +1 @@
+export * from './report-customers-by-date-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts
new file mode 100644
index 000000000000..9d9e83b66d5d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { BaseReportParams } from '../../utils/types';
+
+type ReportsCustomersByDateSummary = {
+ total_net_sales: string;
+ total_gross_sales: string;
+ total_discounts: string;
+ total_refunds: string;
+ total_orders: string;
+ total_average_order_value: string;
+ total_avg_items_per_order: string;
+ total_customers: string;
+ new_customers: string;
+ returning_customers: string;
+ new_customer_sales: string;
+ new_customer_gross_sales: string;
+ new_customer_discounts: string;
+ new_customer_refunds: string;
+ new_customer_orders: string;
+ new_customer_avg_order_value: string;
+ new_customer_avg_items_per_order: string;
+ returning_customer_sales: string;
+ returning_customer_gross_sales: string;
+ returning_customer_discounts: string;
+ returning_customer_refunds: string;
+ returning_customer_orders: string;
+ returning_customer_avg_order_value: string;
+ returning_customer_avg_items_per_order: string;
+ date_start: string;
+ date_end: string;
+};
+
+type CustomersReportDataItem = {
+ time_interval: string;
+ date_start: string;
+ date_end: string;
+ total_customers: string;
+ new_customers: string;
+ returning_customers: string;
+ orders_count: string;
+ new_customer_orders: string;
+ returning_customer_orders: string;
+ net_sales: string;
+ new_customer_net_sales: string;
+ returning_customer_net_sales: string;
+};
+
+type ReportsCustomersByDateResponse = {
+ data: CustomersReportDataItem[];
+ summary: ReportsCustomersByDateSummary;
+};
+
+export type RequestReportCustomersByDateParams = BaseReportParams;
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.date_type
+ */
+export async function fetchReportCustomersByDate( {
+ from,
+ to,
+ interval,
+ date_type,
+}: RequestReportCustomersByDateParams ): Promise< ReportsCustomersByDateResponse > {
+ const path = addQueryArgs( `${ reportsPath }/customers/by-date`, {
+ from,
+ to,
+ interval,
+ date_type,
+ } );
+
+ return apiFetch( { path } ) as Promise< ReportsCustomersByDateResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts
new file mode 100644
index 000000000000..b5d7c2f32fe4
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts
@@ -0,0 +1 @@
+export { fetchReportCustomers, type RequestReportCustomersParams } from './report-customers-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts
new file mode 100644
index 000000000000..1747d7c8cdb4
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts
@@ -0,0 +1,61 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+
+type CustomersNewReturningSummary = {
+ total_net_sales: string;
+ total_orders: string;
+ new_customer_sales: string;
+ returning_customer_sales: string;
+ date_start: string;
+ date_end: string;
+};
+
+type CustomersNewReturningItem = {
+ customer_type: 'new' | 'returning';
+ net_sales: string;
+ orders_count: string;
+};
+
+type ReportsCustomersNewReturningResponse = {
+ summary: CustomersNewReturningSummary;
+ data: CustomersNewReturningItem[];
+};
+
+export type RequestReportCustomersParams = Omit< BaseReportParams, 'interval' > & {
+ filters?: FilterCondition[];
+};
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.filters
+ * @param root0.date_type
+ */
+export async function fetchReportCustomers( {
+ from,
+ to,
+ filters,
+ date_type,
+}: RequestReportCustomersParams ): Promise< ReportsCustomersNewReturningResponse > {
+ const path = addQueryArgs( `${ reportsPath }/customers/new-returning`, {
+ from,
+ to,
+ filters,
+ date_type,
+ } );
+
+ return apiFetch( {
+ path,
+ } ) as Promise< ReportsCustomersNewReturningResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts
new file mode 100644
index 000000000000..4b36b16295d5
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts
@@ -0,0 +1,2 @@
+export { exportReport } from './report-export-fetch';
+export type { ExportReportParams, ExportReportResponse } from './report-export-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts
new file mode 100644
index 000000000000..645bdff72b3f
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts
@@ -0,0 +1,57 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+
+/**
+ * Export request parameters
+ */
+export interface ExportReportParams {
+ reportType: string | string[];
+ from: string; // ISO 8601 date string
+ to: string; // ISO 8601 date string
+ interval?: string;
+ compareFrom?: string; // ISO 8601 date string
+ compareTo?: string; // ISO 8601 date string
+}
+
+/**
+ * Export response from the API
+ */
+export interface ExportReportResponse {
+ success: boolean;
+ message: string;
+ job_ids?: Record< string, number >; // Multiple report exports
+ partial?: boolean; // Indicates if some exports failed
+ errors?: Record< string, string >; // Failed report types and their error messages
+}
+
+/**
+ * Export one or more reports via email
+ *
+ * @param params - Export parameters
+ * @return Promise that resolves to the export response
+ */
+export async function exportReport( params: ExportReportParams ): Promise< ExportReportResponse > {
+ const path = '/wc/v3/woocommerce-analytics/reports/csv-export';
+
+ const body = {
+ report_type: Array.isArray( params.reportType ) ? params.reportType : [ params.reportType ],
+ from: params.from,
+ to: params.to,
+ interval: params.interval || 'day',
+ delivery_method: 'email',
+ ...( params.compareFrom && params.compareTo
+ ? {
+ compare_from: params.compareFrom,
+ compare_to: params.compareTo,
+ }
+ : {} ),
+ };
+
+ return apiFetch( {
+ path,
+ method: 'POST',
+ data: body,
+ } ) as Promise< ExportReportResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts
new file mode 100644
index 000000000000..2039d9b82507
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts
@@ -0,0 +1 @@
+export * from './report-order-attribution-by-product-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts
new file mode 100644
index 000000000000..7e35aff2130e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts
@@ -0,0 +1,76 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+import type { ORDER_ATTRIBUTION_VIEWS } from '../report-order-attribution-summary-fetch/report-order-attribution-summary-fetch';
+
+type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ];
+
+type OrderAttributionByProductInterval = {
+ time_interval: string;
+ date_start: string;
+ date_end: string;
+ net_sales: string;
+};
+
+type OrderAttributionByProductItem = {
+ item: string;
+ value: string;
+ intervals: OrderAttributionByProductInterval[];
+};
+
+export type OrderAttributionByProductResponse = {
+ view: OrderAttributionView;
+ order_by: string;
+ data: OrderAttributionByProductItem[];
+};
+
+export type RequestReportOrderAttributionByProductParams = BaseReportParams & {
+ view: OrderAttributionView;
+ filters?: FilterCondition[];
+};
+
+/**
+ * Fetches order attribution by product data from the WC Analytics REST API
+ *
+ * This endpoint supports product filtering similar to fetchReportOrdersByProductType.
+ * Unlike the regular order-attribution endpoint, this one:
+ * - Does not support compare_from/compare_to parameters
+ * - Returns data in a flatter structure (no current_period/previous_period nesting)
+ * - Requires separate requests for comparison data
+ *
+ * @param params - Query parameters
+ * @return Promise resolving to order attribution by product response
+ */
+export async function fetchReportOrderAttributionByProduct(
+ params: RequestReportOrderAttributionByProductParams
+): Promise< OrderAttributionByProductResponse > {
+ const { from, to, interval, view, filters, date_type } = params;
+
+ const queryParams: Record< string, any > = {
+ from,
+ to,
+ interval,
+ view,
+ date_type,
+ };
+
+ // Add filters to query params if provided
+ if ( filters && filters.length > 0 ) {
+ queryParams.filters = filters;
+ }
+
+ const path = addQueryArgs(
+ `${ reportsPath }/order-attribution-by-product/${ view }/summary`,
+ queryParams
+ );
+
+ return apiFetch< OrderAttributionByProductResponse >( { path } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts
new file mode 100644
index 000000000000..285b15296160
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts
@@ -0,0 +1,6 @@
+export {
+ fetchReportOrderAttributionSummary,
+ ORDER_ATTRIBUTION_VIEWS,
+ type RequestReportOrderAttributionSummaryParams,
+ type OrderAttributionSummaryResponse,
+} from './report-order-attribution-summary-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts
new file mode 100644
index 000000000000..6c62c763f539
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts
@@ -0,0 +1,85 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { BaseReportParams } from '../../utils/types';
+
+export const ORDER_ATTRIBUTION_VIEWS = [
+ 'channel',
+ 'source',
+ 'campaign',
+ 'device',
+ 'channel-source',
+] as const;
+
+type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ];
+
+type OrderAttributionInterval = {
+ time_interval: string;
+ date_start: string;
+ date_end: string;
+ net_sales: string;
+};
+
+type OrderAttributionPeriod = {
+ value: string;
+ intervals: OrderAttributionInterval[];
+};
+
+type OrderAttributionSummaryItem = {
+ item: string;
+ current_period: OrderAttributionPeriod;
+ previous_period: OrderAttributionPeriod;
+};
+
+export type OrderAttributionSummaryResponse = {
+ view: OrderAttributionView;
+ order_by: string;
+ data: OrderAttributionSummaryItem[];
+};
+
+export type RequestReportOrderAttributionSummaryParams = BaseReportParams & {
+ view: OrderAttributionView;
+ compare_from: string;
+ compare_to: string;
+};
+
+/**
+ * Fetches order attribution summary data from the WC Analytics REST API
+ *
+ * Note: Order attribution summary endpoint returns both primary and comparison
+ * data in a single response, unlike orders endpoint which requires
+ * separate requests. The endpoint requires compare_from and compare_to parameters;
+ * when no comparison is needed, it uses the same values as the primary range.
+ *
+ * @param params - Query parameters
+ * @return Promise resolving to order attribution summary response
+ */
+export async function fetchReportOrderAttributionSummary(
+ params: RequestReportOrderAttributionSummaryParams
+): Promise< OrderAttributionSummaryResponse > {
+ const { from, to, interval, view, compare_from, compare_to, date_type } = params;
+
+ /*
+ * Order attribution endpoint requires compare_from and compare_to.
+ * When no comparison is needed, use the same values as primary range.
+ */
+ const queryParams: Record< string, string | undefined > = {
+ from,
+ to,
+ interval,
+ view,
+ compare_from,
+ compare_to,
+ date_type,
+ };
+
+ const path = addQueryArgs( `${ reportsPath }/order-attribution/${ view }/summary`, queryParams );
+
+ return apiFetch< OrderAttributionSummaryResponse >( { path } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts
new file mode 100644
index 000000000000..2762252eb226
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts
@@ -0,0 +1,5 @@
+export {
+ fetchReportOrders,
+ type RequestReportOrdersParams,
+ type ReportsOrdersByDateResponse,
+} from './report-orders-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts
new file mode 100644
index 000000000000..c97e7f3d8aa9
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts
@@ -0,0 +1,77 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { hasProductFilters } from '../../utils/product-filters';
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+import type { BaseReportParams } from '../../utils/types';
+
+type ReportsOrdersByDateSummary = {
+ average_order_value: string;
+ avg_items: string;
+ cogs_amount: string;
+ coupons: string;
+ date_end: string;
+ date_start: string;
+ orders_no: string;
+ orders_value_gross: string;
+ orders_value_net: string;
+ paid_orders_count: string;
+ paid_net_sales: string;
+ product_net_revenue: string;
+ profit_margin: string;
+ refunds: string;
+ total_sales: string;
+ unpaid_orders_count: string;
+ unpaid_net_sales: string;
+};
+
+type OrdersReportDataItem = ReportsOrdersByDateSummary & {
+ time_interval?: string;
+};
+
+export type ReportsOrdersByDateResponse = {
+ data: OrdersReportDataItem[];
+ summary: ReportsOrdersByDateSummary;
+};
+
+export type RequestReportOrdersParams = BaseReportParams & {
+ filters?: FilterCondition[];
+};
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.filters
+ * @param root0.date_type
+ */
+export async function fetchReportOrders( {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+}: RequestReportOrdersParams ): Promise< ReportsOrdersByDateResponse > {
+ const hasProductFiltersValue = hasProductFilters( filters );
+ const apiUrl = hasProductFiltersValue
+ ? `${ reportsPath }/orders-by-product-type/by-date`
+ : `${ reportsPath }/orders/by-date`;
+
+ const path = addQueryArgs( apiUrl, {
+ from,
+ to,
+ interval,
+ filters,
+ date_type,
+ } );
+
+ return apiFetch( { path } ) as Promise< ReportsOrdersByDateResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts
new file mode 100644
index 000000000000..c786309418cf
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts
@@ -0,0 +1,4 @@
+/**
+ * Internal dependencies
+ */
+export { fetchReportProducts, type RequestReportProductsParams } from './report-products-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts
new file mode 100644
index 000000000000..8af88655042d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts
@@ -0,0 +1,73 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { BaseReportParams } from '../../utils/types';
+import { reportsPath } from '../constants';
+import type { FilterCondition } from '../../types/filter-condition';
+
+export type RequestReportProductsParams = Omit< BaseReportParams, 'interval' > & {
+ limit?: number;
+ orderby?: string;
+ order?: 'asc' | 'desc';
+ filters?: FilterCondition[];
+};
+
+type ReportProductsResponse = {
+ data: {
+ product_id: string;
+ product_name: string;
+ product_net_revenue: string;
+ product_gross_revenue: string;
+ product_type: string;
+ orders_count: string;
+ sku: string;
+ total_quantity: string;
+ stock_status: string;
+ }[];
+ summary: {
+ total_orders: string;
+ total_products: string;
+ total_quantity: string;
+ total_revenue: string;
+ };
+};
+
+/**
+ * Fetches products report data from the WooCommerce Analytics API
+ * @param params
+ */
+export async function fetchReportProducts(
+ params: RequestReportProductsParams
+): Promise< ReportProductsResponse > {
+ const queryArgs: Record< string, any > = {
+ from: params.from,
+ to: params.to,
+ date_type: params.date_type,
+ };
+
+ if ( params.limit ) {
+ queryArgs.limit = params.limit;
+ }
+
+ if ( params.orderby ) {
+ queryArgs.orderby = params.orderby;
+ }
+
+ if ( params.order ) {
+ queryArgs.order = params.order;
+ }
+
+ // Add filters to query params if provided
+ if ( params.filters && params.filters.length > 0 ) {
+ queryArgs.filters = params.filters;
+ }
+
+ return apiFetch< ReportProductsResponse >( {
+ path: addQueryArgs( `${ reportsPath }/products`, queryArgs ),
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts
new file mode 100644
index 000000000000..2da42f37b7e6
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts
@@ -0,0 +1,4 @@
+export {
+ fetchReportSessionsByDevice,
+ type RequestReportSessionsByDeviceParams,
+} from './report-sessions-by-device-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts
new file mode 100644
index 000000000000..3909742411dc
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts
@@ -0,0 +1,60 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { BaseReportParams } from '../../utils/types';
+
+/**
+ * Raw response item from the sessions/by-device endpoint.
+ */
+type SessionsByDeviceItem = {
+ device_type: string;
+ active_sessions: string;
+};
+
+/**
+ * Summary data from the sessions/by-device endpoint.
+ */
+type SessionsByDeviceSummary = {
+ active_sessions: string;
+ total_orders: string;
+ date_start: string;
+ date_end: string;
+};
+
+/**
+ * Raw response structure from the sessions/by-device endpoint.
+ */
+type ReportsSessionsByDeviceResponse = {
+ summary: SessionsByDeviceSummary;
+ data: SessionsByDeviceItem[];
+};
+
+export type RequestReportSessionsByDeviceParams = Omit< BaseReportParams, 'interval' >;
+
+/**
+ * Fetch sessions by device type report data.
+ *
+ * This endpoint returns a breakdown of sessions by device category
+ * (Mobile, Desktop, Tablet) for the specified date range.
+ *
+ * @param params - Request parameters
+ * @param params.from - Start date in YYYY-MM-DD format
+ * @param params.to - End date in YYYY-MM-DD format
+ */
+export async function fetchReportSessionsByDevice( {
+ from,
+ to,
+}: RequestReportSessionsByDeviceParams ): Promise< ReportsSessionsByDeviceResponse > {
+ const path = addQueryArgs( `${ reportsPath }/sessions/by-device`, {
+ from,
+ to,
+ } );
+
+ return apiFetch( { path } ) as Promise< ReportsSessionsByDeviceResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts
new file mode 100644
index 000000000000..18115cbe9222
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts
@@ -0,0 +1,4 @@
+export {
+ fetchReportVisitorsByLocation,
+ type RequestReportVisitorsByLocationParams,
+} from './report-visitors-by-location-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts
new file mode 100644
index 000000000000..bbb9c7fa6193
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts
@@ -0,0 +1,67 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { BaseReportParams } from '../../utils/types';
+
+type VisitorsByLocationReportDataItem = {
+ country_code: string;
+ label: string;
+ region?: string;
+ visitors: string;
+};
+
+type ReportsVisitorsByLocationSummary = {
+ visitors: string;
+ date_start: string;
+ date_end: string;
+};
+
+type ReportsVisitorsByLocationResponse = {
+ data: VisitorsByLocationReportDataItem[];
+ summary?: ReportsVisitorsByLocationSummary;
+};
+
+export type RequestReportVisitorsByLocationParams = BaseReportParams & {
+ group_by: 'country' | 'region';
+ country_code?: string;
+ limit?: number;
+};
+
+/**
+ * Fetch visitors grouped by location (country or region) for the selected period.
+ *
+ * This endpoint is proxied through `/wc/v3/woocommerce-analytics/proxy/reports/...`
+ * and ultimately served by wpcom analytics.
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ * @param root0.group_by
+ * @param root0.country_code
+ * @param root0.limit
+ */
+export async function fetchReportVisitorsByLocation( {
+ from,
+ to,
+ interval,
+ group_by,
+ country_code,
+ limit,
+}: RequestReportVisitorsByLocationParams ): Promise< ReportsVisitorsByLocationResponse > {
+ const path = addQueryArgs( `${ reportsPath }/sessions/by-location`, {
+ from,
+ to,
+ interval,
+ group_by,
+ country_code,
+ limit,
+ } );
+
+ return apiFetch( { path } ) as Promise< ReportsVisitorsByLocationResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts
new file mode 100644
index 000000000000..164ef5351eb7
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts
@@ -0,0 +1 @@
+export { fetchReportVisitors, type RequestReportVisitorsParams } from './report-visitors-fetch';
diff --git a/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts
new file mode 100644
index 000000000000..93895fca50cd
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts
@@ -0,0 +1,49 @@
+/**
+ * External dependencies
+ */
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { reportsPath } from '../constants';
+import type { BaseReportParams } from '../../utils/types';
+
+type ReportsVisitorsByDateSummary = {
+ active_sessions: string;
+ date_end: string;
+ date_start: string;
+ visitors: string;
+};
+
+type VisitorsReportDataItem = ReportsVisitorsByDateSummary & {
+ time_interval: string;
+};
+
+type ReportsVisitorsByDateResponse = {
+ data: VisitorsReportDataItem[];
+ summary: ReportsVisitorsByDateSummary;
+};
+
+export type RequestReportVisitorsParams = BaseReportParams;
+
+/**
+ *
+ * @param root0
+ * @param root0.from
+ * @param root0.to
+ * @param root0.interval
+ */
+export async function fetchReportVisitors( {
+ from,
+ to,
+ interval,
+}: RequestReportVisitorsParams ): Promise< ReportsVisitorsByDateResponse > {
+ const path = addQueryArgs( `${ reportsPath }/sessions/by-date`, {
+ from,
+ to,
+ interval,
+ } );
+
+ return apiFetch( { path } ) as Promise< ReportsVisitorsByDateResponse >;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts
new file mode 100644
index 000000000000..ab6da5ee50fb
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts
@@ -0,0 +1,92 @@
+/**
+ * Mock WordPress dependencies so date.ts can load. The select mock
+ * returns site settings with timezone: 'UTC' so getSiteTimezone()
+ * returns UTC, making localTZDate create UTC-aware TZDates.
+ */
+jest.mock( '@wordpress/core-data', () => ( {
+ store: 'core',
+} ) );
+
+jest.mock( '@wordpress/data', () => ( {
+ select: jest.fn( () => ( {
+ getEntityRecord: jest.fn( () => ( { timezone: 'UTC' } ) ),
+ } ) ),
+} ) );
+
+jest.mock( '../../utils/ensure-core-settings', () => ( {
+ ensureCoreSettingsReady: jest.fn( () => Promise.resolve() ),
+} ) );
+/**
+ * Internal dependencies
+ */
+import { getDefaultPreset, getDefaultQueryParams } from '../reports';
+
+describe( 'getDefaultQueryParams - preset override', () => {
+ beforeEach( () => {
+ jest.useFakeTimers();
+ jest.setSystemTime( new Date( '2025-03-15T12:00:00.000Z' ) );
+ } );
+
+ afterEach( () => {
+ jest.useRealTimers();
+ } );
+
+ it( 'defaults to last-30-days when no preset is given', () => {
+ expect( getDefaultQueryParams().preset ).toBe( 'last-30-days' );
+ } );
+
+ it( 'uses today preset when passed', () => {
+ expect( getDefaultQueryParams( false, 'today' ).preset ).toBe( 'today' );
+ } );
+
+ it( 'uses last-7-days preset when passed', () => {
+ expect( getDefaultQueryParams( false, 'last-7-days' ).preset ).toBe( 'last-7-days' );
+ } );
+
+ it( 'uses last-30-days preset when passed', () => {
+ expect( getDefaultQueryParams( false, 'last-30-days' ).preset ).toBe( 'last-30-days' );
+ } );
+} );
+
+describe( 'getDefaultPreset', () => {
+ beforeEach( () => {
+ jest.useFakeTimers();
+ jest.setSystemTime( new Date( '2025-03-15T12:00:00.000Z' ) );
+ } );
+
+ afterEach( () => {
+ jest.useRealTimers();
+ } );
+
+ it( 'returns last-30-days when no launched date', () => {
+ expect( getDefaultPreset() ).toBe( 'last-30-days' );
+ } );
+
+ it( 'returns last-30-days for undefined', () => {
+ expect( getDefaultPreset( undefined ) ).toBe( 'last-30-days' );
+ } );
+
+ it( 'returns today when store launched today', () => {
+ expect( getDefaultPreset( '2025-03-15T00:00:00Z' ) ).toBe( 'today' );
+ } );
+
+ it( 'returns last-7-days when launched 3 days ago', () => {
+ expect( getDefaultPreset( '2025-03-12T00:00:00Z' ) ).toBe( 'last-7-days' );
+ } );
+
+ it( 'returns last-7-days when launched exactly 7 days ago', () => {
+ expect( getDefaultPreset( '2025-03-08T00:00:00Z' ) ).toBe( 'last-7-days' );
+ } );
+
+ it( 'returns last-30-days when launched 8 days ago', () => {
+ expect( getDefaultPreset( '2025-03-07T00:00:00Z' ) ).toBe( 'last-30-days' );
+ } );
+
+ it( 'returns last-30-days when launched months ago', () => {
+ expect( getDefaultPreset( '2024-01-01T00:00:00Z' ) ).toBe( 'last-30-days' );
+ } );
+
+ it( 'returns today when launched in the future', () => {
+ expect( getDefaultPreset( '2025-04-01T00:00:00Z' ) ).toBe( 'today' );
+ } );
+} );
diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/index.ts b/projects/packages/premium-analytics/packages/data/src/defaults/index.ts
new file mode 100644
index 000000000000..1231063dab14
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/defaults/index.ts
@@ -0,0 +1 @@
+export { getDefaultPreset, getDefaultQueryParams } from './reports';
diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts
new file mode 100644
index 000000000000..76813df11046
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts
@@ -0,0 +1,114 @@
+/**
+ * External dependencies
+ */
+import { getComparisonRangeFromPreset } from '@jetpack-premium-analytics/datetime';
+import { differenceInCalendarDays, startOfDay } from 'date-fns';
+/**
+ * Internal dependencies
+ */
+import {
+ localTZDate,
+ dateToISOStringWithLocalTZ,
+ getDefaultIntervalForPeriod,
+ computeDateRangeFromPreset,
+ type PresetType,
+ type ReportParams,
+} from '../utils';
+
+const DEFAULT_PRESET: PresetType = 'last-30-days';
+
+/**
+ * Pick the default date-range preset based on how long
+ * the store has been live.
+ *
+ * - Not launched / unknown → last-30-days (safe default)
+ * - Launched today → today
+ * - Launched ≤ 7 days ago → last-7-days
+ * - Launched > 7 days ago → last-30-days
+ * @param launchedDate
+ */
+export function getDefaultPreset( launchedDate?: string ): PresetType {
+ if ( ! launchedDate ) {
+ return DEFAULT_PRESET;
+ }
+
+ const today = startOfDay( localTZDate() );
+ const launched = startOfDay( localTZDate( launchedDate ) );
+ const daysSinceLaunch = differenceInCalendarDays( today, launched );
+
+ if ( daysSinceLaunch <= 0 ) {
+ return 'today';
+ }
+
+ if ( daysSinceLaunch <= 7 ) {
+ return 'last-7-days';
+ }
+
+ return DEFAULT_PRESET;
+}
+
+/**
+ * Build report query parameters (from, to, interval, preset)
+ * for the given date-range preset. Defaults to `last-30-days`.
+ *
+ * Callers that need a dynamic default (e.g. based on store
+ * age) should resolve the preset externally and pass it in.
+ * @param withComparison
+ * @param preset
+ */
+export const getDefaultQueryParams = (
+ /**
+ * Include previous-period comparison range.
+ */
+ withComparison: boolean = false,
+
+ /**
+ * Date-range preset. Defaults to `last-30-days`.
+ */
+ preset: PresetType = DEFAULT_PRESET
+): ReportParams => {
+ const range = computeDateRangeFromPreset( preset );
+
+ if ( ! range ) {
+ throw new Error( `Unknown preset: ${ preset }` );
+ }
+
+ const { from: fromString, to: toString } = range;
+
+ const interval = getDefaultIntervalForPeriod( undefined, fromString, toString );
+
+ if ( ! withComparison ) {
+ return {
+ from: fromString,
+ to: toString,
+ preset,
+ interval,
+ };
+ }
+
+ const from = localTZDate( new Date( fromString ) );
+ const to = localTZDate( new Date( toString ) );
+
+ const comparisonParams = getComparisonRangeFromPreset(
+ {
+ from,
+ to,
+ },
+ 'previous-period'
+ );
+
+ return {
+ from: fromString,
+ to: toString,
+ preset,
+ interval,
+ compare_from: comparisonParams?.from
+ ? dateToISOStringWithLocalTZ( comparisonParams?.from )
+ : undefined,
+ compare_to: comparisonParams?.to
+ ? dateToISOStringWithLocalTZ( comparisonParams?.to )
+ : undefined,
+ compare_preset: 'previous-period',
+ comp: '1',
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/index.ts b/projects/packages/premium-analytics/packages/data/src/hooks/index.ts
new file mode 100644
index 000000000000..ceb26db69eed
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/index.ts
@@ -0,0 +1,12 @@
+export { useReportOrders } from './use-report-orders';
+export { useReportOrderAttribution } from './use-report-order-attribution';
+export { useReportCoupons } from './use-report-coupons';
+export { useReportCouponsByDate } from './use-report-coupons-by-date';
+export { useReportCustomers } from './use-report-customers';
+export { useReportConversionRate } from './use-report-conversion-rate';
+export { useReportBookings } from './use-report-bookings';
+
+/**
+ * @deprecated Use individual hooks instead: useReportOrders, useReportOrderAttribution, useReportCoupons
+ */
+export { useReport } from './use-report';
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts
new file mode 100644
index 000000000000..4c76b44e3610
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts
@@ -0,0 +1,90 @@
+/**
+ * External dependencies
+ */
+import { useQuery } from '@tanstack/react-query';
+import apiFetch from '@wordpress/api-fetch';
+import { addQueryArgs } from '@wordpress/url';
+/**
+ * Internal dependencies
+ */
+import { sanitizeReportProductsResponse } from '../processing/products';
+import type { ProductImage } from '../types/product-image';
+
+// Infer the product ID type from the sanitized products response
+type ProductId = ReturnType<
+ typeof sanitizeReportProductsResponse
+>[ 'data' ][ number ][ 'product_id' ];
+
+export interface UseProductImagesParams {
+ productIds: ProductId[];
+}
+
+export interface ProductImageResponse {
+ id: number;
+ name: string;
+ images: {
+ id: number;
+ src: string;
+ name: string;
+ alt: string;
+ }[];
+}
+
+/**
+ * Fetches product images from the WooCommerce REST API
+ * @param productIds
+ */
+async function fetchProductImages(
+ productIds: ProductId[]
+): Promise< ( ProductImage & { productId: ProductId } )[] > {
+ if ( ! productIds.length ) {
+ return [];
+ }
+
+ // Use the include parameter to get only the products we need
+ const queryArgs = {
+ include: productIds.join( ',' ),
+ per_page: productIds.length,
+ };
+
+ try {
+ const response = await apiFetch< ProductImageResponse[] >( {
+ path: addQueryArgs( '/wc/v3/products', queryArgs ),
+ } );
+
+ return response.map( product => ( {
+ productId: product.id,
+ imageUrl: product.images?.[ 0 ]?.src || '',
+ imageAlt: product.images?.[ 0 ]?.alt || product.name,
+ } ) );
+ } catch {
+ return [];
+ }
+}
+
+const getProductImagesQueryKey = ( params: UseProductImagesParams ) =>
+ [ 'product-images', params.productIds.sort().join( ',' ) ] as const;
+
+/**
+ * Hook to fetch product images for a list of product IDs
+ * @param params - Object containing the list of product IDs to fetch images for
+ */
+export function useProductImages( params: UseProductImagesParams ) {
+ return useQuery( {
+ queryKey: getProductImagesQueryKey( params ),
+ queryFn: async () => {
+ const images = await fetchProductImages( params.productIds );
+ return images.reduce(
+ ( acc: Record< number, ProductImage >, image: ProductImage & { productId: number } ) => {
+ acc[ image.productId ] = {
+ imageUrl: image.imageUrl,
+ imageAlt: image.imageAlt,
+ };
+ return acc;
+ },
+ {}
+ );
+ },
+ enabled: params.productIds.length > 0,
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts
new file mode 100644
index 000000000000..7c50d401510d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts
@@ -0,0 +1,16 @@
+/**
+ * Internal dependencies
+ */
+import { reportBookingsQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+/**
+ *
+ * @param params
+ */
+export function useReportBookings( params: ReportParams ) {
+ return useReport( p => reportBookingsQuery( p ), params, {
+ disabledComparisonKey: [ 'reports', 'bookings', 'by-date', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts
new file mode 100644
index 000000000000..b27ec3f9360f
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts
@@ -0,0 +1,25 @@
+/**
+ * Internal dependencies
+ */
+import { reportConversionRateQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportConversionRateOptions = {
+ enabled?: boolean;
+};
+
+/**
+ *
+ * @param params
+ * @param options
+ */
+export function useReportConversionRate(
+ params: ReportParams,
+ options?: UseReportConversionRateOptions
+) {
+ return useReport( p => reportConversionRateQuery( p ), params, {
+ enabled: options?.enabled,
+ disabledComparisonKey: [ 'reports', 'conversion-rate', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts
new file mode 100644
index 000000000000..a88b7a7b282b
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts
@@ -0,0 +1,16 @@
+/**
+ * Internal dependencies
+ */
+import { reportCouponsByDateQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+/**
+ *
+ * @param params
+ */
+export function useReportCouponsByDate( params: ReportParams ) {
+ return useReport( p => reportCouponsByDateQuery( p ), params, {
+ disabledComparisonKey: [ 'reports', 'couponsByDate', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts
new file mode 100644
index 000000000000..205e0d4780af
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts
@@ -0,0 +1,16 @@
+/**
+ * Internal dependencies
+ */
+import { reportCouponsQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+/**
+ *
+ * @param params
+ */
+export function useReportCoupons( params: ReportParams ) {
+ return useReport( p => reportCouponsQuery( p ), params, {
+ disabledComparisonKey: [ 'reports', 'coupons', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts
new file mode 100644
index 000000000000..08217edd8423
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts
@@ -0,0 +1,25 @@
+/**
+ * Internal dependencies
+ */
+import { reportCustomersByDateQuery } from '../queries/report-customers-by-date-query';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportCustomersByDateOptions = {
+ enabled?: boolean;
+};
+
+/**
+ *
+ * @param params
+ * @param options
+ */
+export function useReportCustomersByDate(
+ params: ReportParams,
+ options?: UseReportCustomersByDateOptions
+) {
+ return useReport( p => reportCustomersByDateQuery( p ), params, {
+ enabled: options?.enabled,
+ disabledComparisonKey: [ 'reports', 'customers', 'by-date', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts
new file mode 100644
index 000000000000..2fa65e6724ec
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts
@@ -0,0 +1,22 @@
+/**
+ * Internal dependencies
+ */
+import { reportCustomersQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+/**
+ *
+ * @param params
+ */
+export function useReportCustomers( params: ReportParams ) {
+ return useReport( p => reportCustomersQuery( p ), params, {
+ disabledComparisonKey: [
+ 'reports',
+ 'customers',
+ 'new-returning',
+ '__comparison__',
+ 'disabled',
+ ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts
new file mode 100644
index 000000000000..6377f6e68735
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts
@@ -0,0 +1,66 @@
+/**
+ * Internal dependencies
+ */
+import { reportOrderAttributionSummaryQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportOrderAttributionOptions = {
+ enabled?: boolean;
+};
+
+const DISABLED_COMPARISON_KEY = [
+ 'reports',
+ 'order-attribution',
+ '__comparison__',
+ 'included-in-primary',
+];
+
+/**
+ *
+ * @param params
+ * @param options
+ */
+export function useReportOrderAttribution(
+ params: ReportParams,
+ options?: UseReportOrderAttributionOptions
+) {
+ /*
+ * Compare from and to are required for order attribution summary query.
+ * When they aren't provided, use the same dates as the primary period.
+ */
+ const compareFrom = params.compare_from ?? params.from;
+ const compareTo = params.compare_to ?? params.to;
+
+ return useReport(
+ ( p, queryType ) => {
+ // Order attribution requires the view parameter
+ if ( ! params.view ) {
+ return {
+ queryKey: [ 'reports', 'order-attribution', '__disabled__', 'no-view-param' ],
+ enabled: false,
+ };
+ }
+
+ if ( queryType === 'comparison' ) {
+ return {
+ queryKey: DISABLED_COMPARISON_KEY,
+ enabled: false,
+ };
+ }
+
+ return reportOrderAttributionSummaryQuery( {
+ ...p,
+ view: params.view,
+ compare_from: compareFrom,
+ compare_to: compareTo,
+ date_type: params.date_type,
+ } );
+ },
+ params,
+ {
+ enabled: options?.enabled,
+ disabledComparisonKey: DISABLED_COMPARISON_KEY,
+ }
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts
new file mode 100644
index 000000000000..89cf74039adb
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts
@@ -0,0 +1,22 @@
+/**
+ * Internal dependencies
+ */
+import { reportOrdersQuery } from '../queries';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportOrdersOptions = {
+ enabled?: boolean;
+};
+
+/**
+ *
+ * @param params
+ * @param options
+ */
+export function useReportOrders( params: ReportParams, options?: UseReportOrdersOptions ) {
+ return useReport( p => reportOrdersQuery( p ), params, {
+ enabled: options?.enabled,
+ disabledComparisonKey: [ 'reports', 'orders', 'by-date', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts
new file mode 100644
index 000000000000..905477389c79
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts
@@ -0,0 +1,17 @@
+/**
+ * Internal dependencies
+ */
+import { reportProductsQuery } from '../queries/report-products-query';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+/**
+ *
+ * @param params
+ * @param limit
+ */
+export function useReportProducts( params: ReportParams, limit = 5 ) {
+ return useReport( p => reportProductsQuery( { ...p, limit } ), params, {
+ disabledComparisonKey: [ 'reports', 'products', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts
new file mode 100644
index 000000000000..7ac25ef8f69b
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts
@@ -0,0 +1,39 @@
+/**
+ * Internal dependencies
+ */
+import { reportSessionsByDeviceQuery } from '../queries/report-sessions-by-device-query';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportSessionsByDeviceOptions = {
+ enabled?: boolean;
+};
+
+/**
+ * Hook for fetching sessions by device type report data.
+ *
+ * Returns a breakdown of website sessions by device category
+ * (Mobile, Desktop, Tablet) for the specified date range.
+ *
+ * @param params - Report parameters including date range and comparison dates
+ * @param options - Optional configuration
+ *
+ * @example
+ * ```typescript
+ * const { primary, comparison, hasComparison, isLoading, hasData } =
+ * useReportSessionsByDevice( reportParams );
+ *
+ * // Access the data
+ * const { summary, data } = primary.data;
+ * const totalSessions = summary.total_sessions;
+ * ```
+ */
+export function useReportSessionsByDevice(
+ params: ReportParams,
+ options?: UseReportSessionsByDeviceOptions
+) {
+ return useReport( p => reportSessionsByDeviceQuery( p ), params, {
+ enabled: options?.enabled,
+ disabledComparisonKey: [ 'reports', 'sessions', 'by-device', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts
new file mode 100644
index 000000000000..ac9b8146508d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts
@@ -0,0 +1,38 @@
+/**
+ * Internal dependencies
+ */
+import { reportVisitorsByLocationQuery } from '../queries/report-visitors-by-location-query';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportVisitorsByLocationOptions = {
+ enabled?: boolean;
+ groupBy?: 'country' | 'region';
+ countryCode?: string;
+ limit?: number;
+};
+
+/**
+ *
+ * @param params
+ * @param options
+ */
+export function useReportVisitorsByLocation(
+ params: ReportParams,
+ options?: UseReportVisitorsByLocationOptions
+) {
+ return useReport(
+ p =>
+ reportVisitorsByLocationQuery( {
+ ...p,
+ group_by: options?.groupBy ?? 'country',
+ country_code: options?.countryCode,
+ limit: options?.limit,
+ } ),
+ params,
+ {
+ enabled: options?.enabled,
+ disabledComparisonKey: [ 'reports', 'visitors', 'by-location', '__comparison__', 'disabled' ],
+ }
+ );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts
new file mode 100644
index 000000000000..b6eafd7a3f3f
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts
@@ -0,0 +1,22 @@
+/**
+ * Internal dependencies
+ */
+import { reportVisitorsQuery } from '../queries/report-visitors-query';
+import { type ReportParams } from '../utils/search';
+import { useReport } from './use-report';
+
+type UseReportVisitorsOptions = {
+ enabled?: boolean;
+};
+
+/**
+ *
+ * @param params
+ * @param options
+ */
+export function useReportVisitors( params: ReportParams, options?: UseReportVisitorsOptions ) {
+ return useReport( p => reportVisitorsQuery( p ), params, {
+ enabled: options?.enabled,
+ disabledComparisonKey: [ 'reports', 'visitors', 'by-date', '__comparison__', 'disabled' ],
+ } );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts
new file mode 100644
index 000000000000..e72a203f2e80
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts
@@ -0,0 +1,152 @@
+/**
+ * External dependencies
+ */
+import { useQuery, type UseQueryOptions } from '@tanstack/react-query';
+import { useCallback } from 'react';
+/**
+ * Internal dependencies
+ */
+import { hasComparisonEnabled, type ReportParams } from '../utils/search';
+
+type UseReportOptions = {
+ enabled?: boolean;
+ disabledComparisonKey?: string[];
+};
+
+type QueryFactory< TData > = (
+ params: any,
+ queryType: 'primary' | 'comparison'
+) => UseQueryOptions< TData >;
+
+/**
+ * Generic hook for fetching report data with comparison support.
+ *
+ * This hook handles the common pattern of fetching primary and comparison
+ * data for analytics reports. It automatically manages the comparison query
+ * based on the presence of comparison dates in the params.
+ *
+ * @template TData - The type of data returned by the query
+ *
+ * @param queryFactory - Function that creates a query options object from params
+ * @param params - Report parameters including dates, filters, and comparison dates
+ * @param options - Optional configuration
+ * @param options.enabled - Whether the queries should be enabled (default: true)
+ * @param options.disabledComparisonKey - Query key to use when comparison is disabled
+ *
+ * @return Object containing primary and comparison query results
+ *
+ * @example
+ * ```typescript
+ * const { primary, comparison, hasComparison, isLoading, hasData } = useReport(
+ * (params) => reportOrdersQuery(params, hasProductFilters),
+ * reportParams,
+ * {
+ * enabled: true,
+ * disabledComparisonKey: ['reports', 'orders', '__comparison__', 'disabled'],
+ * }
+ * );
+ * ```
+ */
+export function useReport< TData >(
+ queryFactory: QueryFactory< TData >,
+ params: ReportParams,
+ options?: UseReportOptions
+) {
+ const queryEnabled = options?.enabled ?? true;
+ const comparisonEnabled = hasComparisonEnabled( params );
+
+ // Create primary query
+ const primaryQueryOptions = queryFactory(
+ {
+ from: params.from,
+ to: params.to,
+ interval: params.interval,
+ filters: params.filters,
+ date_type: params.date_type,
+ },
+ 'primary'
+ );
+
+ // Create comparison query if comparison is enabled
+ const comparisonQueryOptions = comparisonEnabled
+ ? queryFactory(
+ {
+ from: params.compare_from,
+ to: params.compare_to,
+ interval: params.interval,
+ filters: params.filters,
+ date_type: params.date_type,
+ },
+ 'comparison'
+ )
+ : {
+ queryKey: options?.disabledComparisonKey ?? [ 'reports', '__comparison__', 'disabled' ],
+ };
+
+ const primary = useQuery( {
+ ...primaryQueryOptions,
+ enabled: queryEnabled && ( primaryQueryOptions.enabled ?? true ),
+ } );
+
+ const comparison = useQuery( {
+ ...comparisonQueryOptions,
+ enabled: queryEnabled && comparisonEnabled && ( comparisonQueryOptions.enabled ?? true ),
+ } );
+
+ // Compute common derived states
+ const isLoading = primary.isLoading || comparison.isLoading;
+ const isFetching = primary.isFetching || comparison.isFetching;
+
+ /**
+ * Check if data exists using standardized response fields.
+ *
+ * All sanitized report responses follow a consistent structure:
+ * - `summary`: Always present (aggregated metrics)
+ * - `data`: Array of time-series or items (orders, bookings, products, etc.)
+ * - `steps`: Array of funnel steps (conversion-rate only)
+ *
+ * We check multiple fields because different endpoints return different combinations:
+ * - Time-series reports (orders, bookings, visitors): { summary, data }
+ * - Conversion funnel: { summary, data, steps, overallRate }
+ * - List reports (products, coupons): { summary, data }
+ *
+ * The use of `as any` is intentional here to handle the generic TData type,
+ * since we cannot add constraints to the generic without breaking existing usage.
+ *
+ * Note: With placeholderData enabled in queries, this hasData check is sufficient
+ * to determine loading states:
+ * - If hasData is true: We have data to display (show with loading indicator if fetching)
+ * - If hasData is false: No data to display (show skeleton)
+ */
+ const hasData =
+ Boolean( ( primary.data as any )?.summary ) ||
+ Boolean( ( primary.data as any )?.data?.length ) ||
+ Boolean( ( primary.data as any )?.steps?.length ) ||
+ Boolean( ( comparison.data as any )?.summary ) ||
+ Boolean( ( comparison.data as any )?.data?.length ) ||
+ Boolean( ( comparison.data as any )?.steps?.length );
+
+ // Combined refetch function that refetches both queries.
+ // If both primary and comparison queries fail, clicking "Retry" should refetch both.
+ const primaryRefetch = primary.refetch;
+ const comparisonRefetch = comparison.refetch;
+ const refetch = useCallback( async () => {
+ await Promise.all( [
+ primaryRefetch(),
+ comparisonEnabled ? comparisonRefetch() : Promise.resolve(),
+ ] );
+ }, [ comparisonEnabled, primaryRefetch, comparisonRefetch ] );
+
+ return {
+ primary,
+ comparison,
+ hasComparison: comparisonEnabled,
+ isLoading,
+ isFetching,
+ hasData,
+ // Error handling
+ isError: primary.isError || comparison.isError,
+ error: primary.error ?? comparison.error,
+ refetch,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/index.ts b/projects/packages/premium-analytics/packages/data/src/index.ts
new file mode 100644
index 000000000000..6f02580c031e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/index.ts
@@ -0,0 +1,41 @@
+export { AnalyticsQueryClientProvider, queryClient } from './providers/query-client-provider';
+export { GlobalErrorProvider, useGlobalError } from './providers/global-error-context';
+export { globalErrorManager, type GlobalErrorType } from './providers/global-error-manager';
+export { useReportOrders } from './hooks/use-report-orders';
+export { useReportOrderAttribution } from './hooks/use-report-order-attribution';
+export { useReportCoupons } from './hooks/use-report-coupons';
+export { useReportCouponsByDate } from './hooks/use-report-coupons-by-date';
+export { useReportCustomers } from './hooks/use-report-customers';
+export { useReportCustomersByDate } from './hooks/use-report-customers-by-date';
+export { useReportConversionRate } from './hooks/use-report-conversion-rate';
+export { useReportProducts } from './hooks/use-report-products';
+export { useProductImages } from './hooks/use-product-images';
+export { useReportVisitors } from './hooks/use-report-visitors';
+export { useReportVisitorsByLocation } from './hooks/use-report-visitors-by-location';
+export { useReportBookings } from './hooks/use-report-bookings';
+export { useReportSessionsByDevice } from './hooks/use-report-sessions-by-device';
+export { prefetchReport } from './prefetch';
+export {
+ normalizeReportParams,
+ hasComparisonEnabled,
+ type PresetType,
+ type ReportParams,
+} from './utils/search';
+export {
+ dateToISOStringWithLocalTZ,
+ ensureCoreSettingsReady,
+ getSiteTimezone,
+ getSiteGmtOffset,
+ localTZDate,
+ hasProductFilters,
+ isSelectablePreset,
+} from './utils';
+export type { ReportDataMap } from './types';
+export type { ReportQueryParams } from './api';
+export type { FilterCondition } from './types/filter-condition';
+export type { ProductType } from './types/product-type';
+export { ORDER_ATTRIBUTION_VIEWS } from './api/report-order-attribution-summary-fetch';
+export { getDefaultIntervalForPeriod, getDateFormatFromInterval } from './utils/interval';
+export { getDefaultPreset, getDefaultQueryParams } from './defaults';
+export { exportReport } from './api';
+export type { ExportReportParams, ExportReportResponse } from './api';
diff --git a/projects/packages/premium-analytics/packages/data/src/prefetch/index.ts b/projects/packages/premium-analytics/packages/data/src/prefetch/index.ts
new file mode 100644
index 000000000000..88eaef8a0f2a
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/prefetch/index.ts
@@ -0,0 +1 @@
+export { prefetchReport } from './prefetch-report';
diff --git a/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts
new file mode 100644
index 000000000000..b73aac40dca0
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts
@@ -0,0 +1,103 @@
+/**
+ * Internal dependencies
+ */
+import { queryClient } from '../providers';
+import {
+ reportOrdersQuery,
+ reportOrderAttributionSummaryQuery,
+ reportCouponsQuery,
+ reportCouponsByDateQuery,
+ reportCustomersQuery,
+ reportCustomersByDateQuery,
+ reportVisitorsQuery,
+ reportVisitorsByLocationQuery,
+ reportSessionsByDeviceQuery,
+ reportProductsQuery,
+ reportConversionRateQuery,
+} from '../queries';
+
+type RequestReportParamsMap = {
+ orders: Parameters< typeof reportOrdersQuery >[ 0 ];
+ 'order-attribution': Parameters< typeof reportOrderAttributionSummaryQuery >[ 0 ];
+ coupons: Parameters< typeof reportCouponsQuery >[ 0 ];
+ 'coupons-by-date': Parameters< typeof reportCouponsByDateQuery >[ 0 ];
+ customers: Parameters< typeof reportCustomersQuery >[ 0 ];
+ 'customers-by-date': Parameters< typeof reportCustomersByDateQuery >[ 0 ];
+ visitors: Parameters< typeof reportVisitorsQuery >[ 0 ];
+ 'visitors-by-location': Parameters< typeof reportVisitorsByLocationQuery >[ 0 ];
+ 'sessions-by-device': Parameters< typeof reportSessionsByDeviceQuery >[ 0 ];
+ products: Parameters< typeof reportProductsQuery >[ 0 ];
+ 'conversion-rate': Parameters< typeof reportConversionRateQuery >[ 0 ];
+};
+
+/**
+ *
+ * @param reportType
+ * @param params
+ */
+export async function prefetchReport< T extends keyof RequestReportParamsMap >(
+ reportType: T = 'orders' as T,
+ params: RequestReportParamsMap[ T ]
+) {
+ switch ( reportType ) {
+ case 'orders':
+ return queryClient.ensureQueryData(
+ reportOrdersQuery( params as RequestReportParamsMap[ 'orders' ] )
+ );
+
+ case 'order-attribution':
+ return queryClient.ensureQueryData(
+ reportOrderAttributionSummaryQuery(
+ params as RequestReportParamsMap[ 'order-attribution' ]
+ )
+ );
+
+ case 'coupons':
+ return queryClient.ensureQueryData(
+ reportCouponsQuery( params as RequestReportParamsMap[ 'coupons' ] )
+ );
+
+ case 'coupons-by-date':
+ return queryClient.ensureQueryData(
+ reportCouponsByDateQuery( params as RequestReportParamsMap[ 'coupons-by-date' ] )
+ );
+
+ case 'customers':
+ return queryClient.ensureQueryData(
+ reportCustomersQuery( params as RequestReportParamsMap[ 'customers' ] )
+ );
+
+ case 'customers-by-date':
+ return queryClient.ensureQueryData(
+ reportCustomersByDateQuery( params as RequestReportParamsMap[ 'customers-by-date' ] )
+ );
+
+ case 'visitors':
+ return queryClient.ensureQueryData(
+ reportVisitorsQuery( params as RequestReportParamsMap[ 'visitors' ] )
+ );
+
+ case 'visitors-by-location':
+ return queryClient.ensureQueryData(
+ reportVisitorsByLocationQuery( params as RequestReportParamsMap[ 'visitors-by-location' ] )
+ );
+
+ case 'sessions-by-device':
+ return queryClient.ensureQueryData(
+ reportSessionsByDeviceQuery( params as RequestReportParamsMap[ 'sessions-by-device' ] )
+ );
+
+ case 'products':
+ return queryClient.ensureQueryData(
+ reportProductsQuery( params as RequestReportParamsMap[ 'products' ] )
+ );
+
+ case 'conversion-rate':
+ return queryClient.ensureQueryData(
+ reportConversionRateQuery( params as RequestReportParamsMap[ 'conversion-rate' ] )
+ );
+
+ default:
+ throw new Error( `Unsupported report type: ${ reportType }` );
+ }
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts
new file mode 100644
index 000000000000..7073cb9e7d32
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts
@@ -0,0 +1,105 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportBookings } from '../../api/report-bookings-fetch';
+import { safeParseInt } from '../../utils/parsing';
+import type { Override } from '../../utils/types';
+
+type ReportsBookingsByDateResponse = Awaited< ReturnType< typeof fetchReportBookings > >;
+type RawBookingsReportDataItem = ReportsBookingsByDateResponse[ 'data' ][ number ];
+type RawBookingsReportSummaryItem = ReportsBookingsByDateResponse[ 'summary' ];
+
+type SanitizedBookingsByDateItem = Override<
+ RawBookingsReportDataItem,
+ {
+ status_unpaid: number;
+ status_pending_confirmation: number;
+ status_confirmed: number;
+ status_paid: number;
+ status_cancelled: number;
+ status_complete: number;
+ attendance_status_booked: number;
+ attendance_status_no_show: number;
+ attendance_status_checked_in: number;
+ }
+>;
+
+type SanitizedBookingsSummaryItem = Override<
+ RawBookingsReportSummaryItem,
+ {
+ status_unpaid: number;
+ status_pending_confirmation: number;
+ status_confirmed: number;
+ status_paid: number;
+ status_cancelled: number;
+ status_complete: number;
+ attendance_status_booked: number;
+ attendance_status_no_show: number;
+ attendance_status_checked_in: number;
+ }
+>;
+
+/**
+ * Sanitize/process a single booking item by converting strings to numbers
+ * @param item
+ */
+function sanitizeBookingItem( item: RawBookingsReportDataItem ): SanitizedBookingsByDateItem {
+ return {
+ ...item,
+ status_unpaid: safeParseInt( item.status_unpaid ),
+ status_pending_confirmation: safeParseInt( item.status_pending_confirmation ),
+ status_confirmed: safeParseInt( item.status_confirmed ),
+ status_paid: safeParseInt( item.status_paid ),
+ status_cancelled: safeParseInt( item.status_cancelled ),
+ status_complete: safeParseInt( item.status_complete ),
+ attendance_status_booked: safeParseInt( item.attendance_status_booked ),
+ attendance_status_no_show: safeParseInt( item.attendance_status_no_show ),
+ attendance_status_checked_in: safeParseInt( item.attendance_status_checked_in ),
+ };
+}
+
+/**
+ * Sanitize/process a single booking summary item by converting strings to numbers
+ * @param item
+ */
+function sanitizeBookingSummaryItem(
+ item: RawBookingsReportSummaryItem
+): SanitizedBookingsSummaryItem {
+ return {
+ ...item,
+ status_unpaid: safeParseInt( item.status_unpaid ),
+ status_pending_confirmation: safeParseInt( item.status_pending_confirmation ),
+ status_confirmed: safeParseInt( item.status_confirmed ),
+ status_paid: safeParseInt( item.status_paid ),
+ status_cancelled: safeParseInt( item.status_cancelled ),
+ status_complete: safeParseInt( item.status_complete ),
+ attendance_status_booked: safeParseInt( item.attendance_status_booked ),
+ attendance_status_no_show: safeParseInt( item.attendance_status_no_show ),
+ attendance_status_checked_in: safeParseInt( item.attendance_status_checked_in ),
+ };
+}
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedBookingsByDateResponse = {
+ summary: SanitizedBookingsSummaryItem;
+ data: SanitizedBookingsByDateItem[];
+};
+
+/**
+ * Sanitize the response from the reports/bookings/by-date endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ *
+ * The `summary` and `data` items have different structures (summary lacks time_interval),
+ * so we use different sanitizer functions for each.
+ * @param response
+ */
+export const sanitizeReportBookingsResponse = (
+ response: ReportsBookingsByDateResponse
+): SanitizedBookingsByDateResponse => {
+ return {
+ summary: sanitizeBookingSummaryItem( response.summary ),
+ data: response.data.map( sanitizeBookingItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts
new file mode 100644
index 000000000000..1135cf3a43f0
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts
@@ -0,0 +1,144 @@
+/**
+ * External dependencies
+ */
+import { __ } from '@wordpress/i18n';
+/**
+ * Internal dependencies
+ */
+import { fetchReportConversionRate } from '../../api/report-conversion-rate-fetch';
+import { safeParseInt } from '../../utils/parsing';
+import type { Override } from '../../utils/types';
+
+type ReportsConversionRateByDateResponse = Awaited<
+ ReturnType< typeof fetchReportConversionRate >
+>;
+type RawConversionRateReportDataItem = ReportsConversionRateByDateResponse[ 'data' ][ number ];
+type SanitizedConversionRateByDateItem = Override<
+ RawConversionRateReportDataItem,
+ {
+ active_sessions: number;
+ visitors: number;
+ with_cart_addition: number;
+ reached_checkout: number;
+ completed_checkout: number;
+ conversion_rate: number; // calculated field
+ }
+>;
+
+/**
+ * Sanitize/process a single conversion rate item by converting strings to numbers
+ * and calculating the conversion rate
+ * @param item
+ */
+function sanitizeConversionRateItem(
+ item: RawConversionRateReportDataItem
+): SanitizedConversionRateByDateItem {
+ const activeSessionsNum = safeParseInt( item.active_sessions );
+ const visitorsNum = safeParseInt( item.visitors );
+ const withCartAdditionNum = safeParseInt( item.with_cart_addition );
+ const reachedCheckoutNum = safeParseInt( item.reached_checkout );
+ const completedCheckoutNum = safeParseInt( item.completed_checkout );
+
+ // Calculate conversion rate as decimal (e.g., 0.035 for 3.5%)
+ // This format works with formatMetricValue 'percentage' type
+ const conversionRate = activeSessionsNum > 0 ? completedCheckoutNum / activeSessionsNum : 0;
+
+ return {
+ ...item,
+ active_sessions: activeSessionsNum,
+ visitors: visitorsNum,
+ with_cart_addition: withCartAdditionNum,
+ reached_checkout: reachedCheckoutNum,
+ completed_checkout: completedCheckoutNum,
+ conversion_rate: conversionRate,
+ };
+}
+
+/**
+ * Funnel step for conversion rate visualization
+ */
+type FunnelStep = {
+ id: string;
+ label: string;
+ count: number;
+ rate: number;
+};
+
+/**
+ * Processed response with funnel steps and overall conversion rate
+ */
+type SanitizedConversionRateByDateResponse = {
+ summary: SanitizedConversionRateByDateItem;
+ data: SanitizedConversionRateByDateItem[];
+ steps: FunnelStep[];
+ overallRate: number;
+};
+
+/**
+ * Sanitize the response from the sessions/by-conversion-rate endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ *
+ * The `summary` single item has basically the same structure
+ * as the `data` array items, so we can use the same mapper function for both.
+ * @param response
+ */
+export const sanitizeReportConversionRateResponse = (
+ response: ReportsConversionRateByDateResponse
+): SanitizedConversionRateByDateResponse => {
+ // Handle cases where response might not have the expected structure
+ const defaultSummary = {
+ active_sessions: '0',
+ visitors: '0',
+ with_cart_addition: '0',
+ reached_checkout: '0',
+ completed_checkout: '0',
+ date_start: '',
+ date_end: '',
+ };
+
+ const sanitizedSummary = sanitizeConversionRateItem( response?.summary || defaultSummary );
+
+ // Create funnel steps from the summary data
+ const steps: FunnelStep[] = [
+ {
+ id: 'sessions',
+ label: __( 'Sessions', 'jetpack-premium-analytics' ),
+ count: sanitizedSummary.active_sessions,
+ rate: 100, // Starting point
+ },
+ {
+ id: 'cart-addition',
+ label: __( 'Cart', 'jetpack-premium-analytics' ),
+ count: sanitizedSummary.with_cart_addition,
+ rate:
+ sanitizedSummary.active_sessions > 0
+ ? ( sanitizedSummary.with_cart_addition / sanitizedSummary.active_sessions ) * 100
+ : 0,
+ },
+ {
+ id: 'checkout',
+ label: __( 'Checkout', 'jetpack-premium-analytics' ),
+ count: sanitizedSummary.reached_checkout,
+ rate:
+ sanitizedSummary.active_sessions > 0
+ ? ( sanitizedSummary.reached_checkout / sanitizedSummary.active_sessions ) * 100
+ : 0,
+ },
+ {
+ id: 'completed',
+ label: __( 'Purchase', 'jetpack-premium-analytics' ),
+ count: sanitizedSummary.completed_checkout,
+ rate:
+ sanitizedSummary.active_sessions > 0
+ ? ( sanitizedSummary.completed_checkout / sanitizedSummary.active_sessions ) * 100
+ : 0,
+ },
+ ];
+
+ return {
+ summary: sanitizedSummary,
+ data: response?.data ? response.data.map( sanitizeConversionRateItem ) : [],
+ steps,
+ overallRate: sanitizedSummary.conversion_rate,
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts
new file mode 100644
index 000000000000..c8af73181fce
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts
@@ -0,0 +1,105 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportCouponsByDate } from '../../api/report-coupons-by-date-fetch';
+import type { Override } from '../../utils/types';
+
+type ReportsCouponsByDateResponse = Awaited< ReturnType< typeof fetchReportCouponsByDate > >;
+type RawSummary = ReportsCouponsByDateResponse[ 'summary' ];
+type RawDataItem = ReportsCouponsByDateResponse[ 'data' ][ number ];
+
+/**
+ * Processed summary with numeric values.
+ */
+type SanitizedCouponsByDateSummary = Override<
+ RawSummary,
+ {
+ total_orders: number;
+ orders_with_coupon: number;
+ orders_without_coupon: number;
+ total_sales: number;
+ sales_with_coupon: number;
+ sales_without_coupon: number;
+ total_discount_amount: number;
+ net_sales_after_discount: number;
+ coupon_usage_percentage: number;
+ }
+>;
+
+/**
+ * Processed data item with numeric values.
+ */
+type SanitizedCouponsByDateDataItem = Override<
+ RawDataItem,
+ {
+ total_orders: number;
+ orders_with_coupon: number;
+ orders_without_coupon: number;
+ total_sales: number;
+ sales_with_coupon: number;
+ sales_without_coupon: number;
+ total_discount_amount: number;
+ net_sales_after_discount: number;
+ coupon_usage_percentage: number;
+ }
+>;
+
+/**
+ * Processed response with numeric values.
+ */
+type SanitizedCouponsByDateResponse = {
+ summary: SanitizedCouponsByDateSummary;
+ data: SanitizedCouponsByDateDataItem[];
+};
+
+/**
+ *
+ * @param item
+ */
+function sanitizeItem( item: RawDataItem ): SanitizedCouponsByDateDataItem {
+ return {
+ ...item,
+ total_orders: parseInt( item.total_orders, 10 ),
+ orders_with_coupon: parseInt( item.orders_with_coupon, 10 ),
+ orders_without_coupon: parseInt( item.orders_without_coupon, 10 ),
+ total_sales: parseFloat( item.total_sales ),
+ sales_with_coupon: parseFloat( item.sales_with_coupon ),
+ sales_without_coupon: parseFloat( item.sales_without_coupon ),
+ total_discount_amount: parseFloat( item.total_discount_amount ),
+ net_sales_after_discount: parseFloat( item.net_sales_after_discount ),
+ coupon_usage_percentage: parseFloat( item.coupon_usage_percentage ),
+ };
+}
+
+/**
+ *
+ * @param summary
+ */
+function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary {
+ return {
+ ...summary,
+ total_orders: parseInt( summary.total_orders, 10 ),
+ orders_with_coupon: parseInt( summary.orders_with_coupon, 10 ),
+ orders_without_coupon: parseInt( summary.orders_without_coupon, 10 ),
+ total_sales: parseFloat( summary.total_sales ),
+ sales_with_coupon: parseFloat( summary.sales_with_coupon ),
+ sales_without_coupon: parseFloat( summary.sales_without_coupon ),
+ total_discount_amount: parseFloat( summary.total_discount_amount ),
+ net_sales_after_discount: parseFloat( summary.net_sales_after_discount ),
+ coupon_usage_percentage: parseFloat( summary.coupon_usage_percentage ),
+ };
+}
+
+/**
+ * Sanitize the response from the reports/coupons/by-date endpoint.
+ * Converts string values to numbers for calculations and charting.
+ * @param response
+ */
+export const sanitizeReportCouponsByDateResponse = (
+ response: ReportsCouponsByDateResponse
+): SanitizedCouponsByDateResponse => {
+ return {
+ summary: sanitizeSummary( response.summary ),
+ data: response.data.map( sanitizeItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts
new file mode 100644
index 000000000000..1f40db489cd3
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts
@@ -0,0 +1,81 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportCoupons } from '../../api/report-coupons-fetch';
+import type { Override } from '../../utils/types';
+
+type ReportsCouponsResponse = Awaited< ReturnType< typeof fetchReportCoupons > >;
+type RawCouponsDataItem = ReportsCouponsResponse[ 'data' ][ number ];
+type RawCouponsDataSummary = ReportsCouponsResponse[ 'summary' ];
+
+/**
+ * Processed data item (numbers for calculations)
+ */
+type SanitizedCouponsDataItem = Override<
+ RawCouponsDataItem,
+ {
+ discount_amount: number;
+ total_sales: number;
+ orders_count: number;
+ }
+>;
+
+/**
+ * Processed summary (numbers for calculations)
+ */
+type SanitizedCouponsDataSummary = Override<
+ RawCouponsDataSummary,
+ {
+ total_sales: number;
+ total_discount_amount: number;
+ total_orders: number;
+ }
+>;
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedCouponsResponse = {
+ summary: SanitizedCouponsDataSummary;
+ data: SanitizedCouponsDataItem[];
+};
+
+/**
+ * Sanitize/process a single coupon item by converting strings to numbers
+ * @param item
+ */
+function sanitizeCouponItem( item: RawCouponsDataItem ): SanitizedCouponsDataItem {
+ return {
+ ...item,
+ discount_amount: parseFloat( item.discount_amount ),
+ total_sales: parseFloat( item.total_sales ),
+ orders_count: parseInt( item.orders_count, 10 ),
+ };
+}
+
+/**
+ * Sanitize/process summary by converting strings to numbers
+ * @param summary
+ */
+function sanitizeCouponSummary( summary: RawCouponsDataSummary ): SanitizedCouponsDataSummary {
+ return {
+ ...summary,
+ total_sales: parseFloat( summary.total_sales ),
+ total_discount_amount: parseFloat( summary.total_discount_amount ),
+ total_orders: parseInt( summary.total_orders, 10 ),
+ };
+}
+
+/**
+ * Sanitize the response from the reports/coupons endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ * @param response
+ */
+export const sanitizeReportCouponsResponse = (
+ response: ReportsCouponsResponse
+): SanitizedCouponsResponse => {
+ return {
+ summary: sanitizeCouponSummary( response.summary ),
+ data: response.data.map( sanitizeCouponItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts
new file mode 100644
index 000000000000..8b098d7adde2
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts
@@ -0,0 +1,149 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportCustomersByDate } from '../../api/report-customers-by-date-fetch';
+import type { Override } from '../../utils/types';
+
+type ReportsCustomersByDateResponse = Awaited< ReturnType< typeof fetchReportCustomersByDate > >;
+type RawCustomersByDateSummary = ReportsCustomersByDateResponse[ 'summary' ];
+type RawCustomersByDateItem = ReportsCustomersByDateResponse[ 'data' ][ number ];
+
+/**
+ * Processed summary (numbers for calculations)
+ */
+type SanitizedCustomersByDateSummary = Override<
+ RawCustomersByDateSummary,
+ {
+ total_net_sales: number;
+ total_gross_sales: number;
+ total_discounts: number;
+ total_refunds: number;
+ total_orders: number;
+ total_average_order_value: number;
+ total_avg_items_per_order: number;
+ total_customers: number;
+ new_customers: number;
+ returning_customers: number;
+ new_customer_sales: number;
+ new_customer_gross_sales: number;
+ new_customer_discounts: number;
+ new_customer_refunds: number;
+ new_customer_orders: number;
+ new_customer_avg_order_value: number;
+ new_customer_avg_items_per_order: number;
+ returning_customer_sales: number;
+ returning_customer_gross_sales: number;
+ returning_customer_discounts: number;
+ returning_customer_refunds: number;
+ returning_customer_orders: number;
+ returning_customer_avg_order_value: number;
+ returning_customer_avg_items_per_order: number;
+ // Add computed field for compatibility
+ customers: number;
+ }
+>;
+
+/**
+ * Processed item (numbers for calculations)
+ */
+type SanitizedCustomersByDateItem = Override<
+ RawCustomersByDateItem,
+ {
+ total_customers: number;
+ new_customers: number;
+ returning_customers: number;
+ orders_count: number;
+ new_customer_orders: number;
+ returning_customer_orders: number;
+ net_sales: number;
+ new_customer_net_sales: number;
+ returning_customer_net_sales: number;
+ // Add computed field for compatibility
+ customers: number;
+ }
+>;
+
+/**
+ * Processed response with numeric values
+ */
+export type SanitizedCustomersByDateResponse = {
+ summary: SanitizedCustomersByDateSummary;
+ data: SanitizedCustomersByDateItem[];
+};
+
+/**
+ * Sanitize/process a single customer item by converting strings to numbers
+ * @param item
+ */
+function sanitizeCustomerByDateItem( item: RawCustomersByDateItem ): SanitizedCustomersByDateItem {
+ const totalCustomers = parseInt( item.total_customers, 10 );
+ return {
+ ...item,
+ total_customers: totalCustomers,
+ new_customers: parseInt( item.new_customers, 10 ),
+ returning_customers: parseInt( item.returning_customers, 10 ),
+ orders_count: parseInt( item.orders_count, 10 ),
+ new_customer_orders: parseInt( item.new_customer_orders, 10 ),
+ returning_customer_orders: parseInt( item.returning_customer_orders, 10 ),
+ net_sales: parseFloat( item.net_sales ),
+ new_customer_net_sales: parseFloat( item.new_customer_net_sales ),
+ returning_customer_net_sales: parseFloat( item.returning_customer_net_sales ),
+ // Add alias for compatibility with chart builder
+ customers: totalCustomers,
+ };
+}
+
+/**
+ * Sanitize/process the summary by converting strings to numbers
+ * @param summary
+ */
+function sanitizeCustomerByDateSummary(
+ summary: RawCustomersByDateSummary
+): SanitizedCustomersByDateSummary {
+ const totalCustomers = parseInt( summary.total_customers, 10 );
+ return {
+ ...summary,
+ total_net_sales: parseFloat( summary.total_net_sales ),
+ total_gross_sales: parseFloat( summary.total_gross_sales ),
+ total_discounts: parseFloat( summary.total_discounts ),
+ total_refunds: parseFloat( summary.total_refunds ),
+ total_orders: parseInt( summary.total_orders, 10 ),
+ total_average_order_value: parseFloat( summary.total_average_order_value ),
+ total_avg_items_per_order: parseFloat( summary.total_avg_items_per_order ),
+ total_customers: totalCustomers,
+ new_customers: parseInt( summary.new_customers, 10 ),
+ returning_customers: parseInt( summary.returning_customers, 10 ),
+ new_customer_sales: parseFloat( summary.new_customer_sales ),
+ new_customer_gross_sales: parseFloat( summary.new_customer_gross_sales ),
+ new_customer_discounts: parseFloat( summary.new_customer_discounts ),
+ new_customer_refunds: parseFloat( summary.new_customer_refunds ),
+ new_customer_orders: parseInt( summary.new_customer_orders, 10 ),
+ new_customer_avg_order_value: parseFloat( summary.new_customer_avg_order_value ),
+ new_customer_avg_items_per_order: parseFloat( summary.new_customer_avg_items_per_order ),
+ returning_customer_sales: parseFloat( summary.returning_customer_sales ),
+ returning_customer_gross_sales: parseFloat( summary.returning_customer_gross_sales ),
+ returning_customer_discounts: parseFloat( summary.returning_customer_discounts ),
+ returning_customer_refunds: parseFloat( summary.returning_customer_refunds ),
+ returning_customer_orders: parseInt( summary.returning_customer_orders, 10 ),
+ returning_customer_avg_order_value: parseFloat( summary.returning_customer_avg_order_value ),
+ returning_customer_avg_items_per_order: parseFloat(
+ summary.returning_customer_avg_items_per_order
+ ),
+ // Add alias for compatibility with chart builder
+ customers: totalCustomers,
+ };
+}
+
+/**
+ * Sanitize the response from the reports/customers/by-date endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ * @param response
+ */
+export const sanitizeReportCustomersByDateResponse = (
+ response: ReportsCustomersByDateResponse
+): SanitizedCustomersByDateResponse => {
+ return {
+ summary: sanitizeCustomerByDateSummary( response.summary ),
+ data: response.data.map( sanitizeCustomerByDateItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts
new file mode 100644
index 000000000000..780eca7c831f
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts
@@ -0,0 +1,85 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportCustomers } from '../../api/report-customers-fetch';
+import type { Override } from '../../utils/types';
+
+type ReportsCustomersNewReturningResponse = Awaited< ReturnType< typeof fetchReportCustomers > >;
+type RawCustomersNewReturningSummary = ReportsCustomersNewReturningResponse[ 'summary' ];
+type RawCustomersNewReturningItem = ReportsCustomersNewReturningResponse[ 'data' ][ number ];
+
+/**
+ * Processed summary (numbers for calculations)
+ */
+type SanitizedCustomersNewReturningSummary = Override<
+ RawCustomersNewReturningSummary,
+ {
+ total_net_sales: number;
+ total_orders: number;
+ new_customer_sales: number;
+ returning_customer_sales: number;
+ }
+>;
+
+/**
+ * Processed item (numbers for calculations)
+ */
+type SanitizedCustomersNewReturningItem = Override<
+ RawCustomersNewReturningItem,
+ {
+ net_sales: number;
+ orders_count: number;
+ }
+>;
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedCustomersNewReturningResponse = {
+ summary: SanitizedCustomersNewReturningSummary;
+ data: SanitizedCustomersNewReturningItem[];
+};
+
+/**
+ * Sanitize/process a single customer item by converting strings to numbers
+ * @param item
+ */
+function sanitizeCustomerItem(
+ item: RawCustomersNewReturningItem
+): SanitizedCustomersNewReturningItem {
+ return {
+ ...item,
+ net_sales: parseFloat( item.net_sales ),
+ orders_count: parseInt( item.orders_count, 10 ),
+ };
+}
+
+/**
+ * Sanitize/process the summary by converting strings to numbers
+ * @param summary
+ */
+function sanitizeCustomerSummary(
+ summary: RawCustomersNewReturningSummary
+): SanitizedCustomersNewReturningSummary {
+ return {
+ ...summary,
+ total_net_sales: parseFloat( summary.total_net_sales ),
+ total_orders: parseInt( summary.total_orders, 10 ),
+ new_customer_sales: parseFloat( summary.new_customer_sales ),
+ returning_customer_sales: parseFloat( summary.returning_customer_sales ),
+ };
+}
+
+/**
+ * Sanitize the response from the reports/customers/new-returning endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ * @param response
+ */
+export const sanitizeReportCustomersResponse = (
+ response: ReportsCustomersNewReturningResponse
+): SanitizedCustomersNewReturningResponse => {
+ return {
+ summary: sanitizeCustomerSummary( response.summary ),
+ data: response.data.map( sanitizeCustomerItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/index.ts
new file mode 100644
index 000000000000..2e9bf6a848fa
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/index.ts
@@ -0,0 +1,12 @@
+// Resource-specific processing
+export * from './orders';
+export * from './customers';
+export * from './products';
+export * from './visitors';
+export * from './visitors-by-location';
+
+// TODO: Add coupons processing functions
+// export * from './coupons';
+
+// TODO: Add order attribution processing functions
+// export * from './order-attribution';
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts
new file mode 100644
index 000000000000..c98abdb066b7
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts
@@ -0,0 +1,2 @@
+export * from './sanitize-order-attribution-summary-response';
+export * from './normalize-order-attribution-by-product-response';
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts
new file mode 100644
index 000000000000..5ed83bf8404e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts
@@ -0,0 +1,82 @@
+/**
+ * Internal dependencies
+ */
+import type { OrderAttributionByProductResponse } from '../../api/report-order-attribution-by-product-fetch';
+import type { OrderAttributionSummaryResponse } from '../../api/report-order-attribution-summary-fetch';
+
+/**
+ * Normalizes the order-attribution-by-product API response to match the
+ * structure of the regular order-attribution API response.
+ *
+ * The new API has a flatter structure without current_period/previous_period nesting,
+ * so we need to transform it to match the expected format for widgets.
+ *
+ * @param currentResponse - Response from the current period request
+ * @param previousResponse - Optional response from the comparison period request
+ * @return Normalized response matching OrderAttributionSummaryResponse structure
+ */
+export function normalizeOrderAttributionByProductResponse(
+ currentResponse: OrderAttributionByProductResponse,
+ previousResponse?: OrderAttributionByProductResponse
+): OrderAttributionSummaryResponse {
+ // Create a map for quick lookup of previous period data by item
+ const previousDataMap = new Map< string, ( typeof currentResponse.data )[ 0 ] >();
+ if ( previousResponse ) {
+ previousResponse.data.forEach( item => {
+ previousDataMap.set( item.item, item );
+ } );
+ }
+
+ // Transform the flat structure to nested structure
+ const normalizedData = currentResponse.data.map( currentItem => {
+ const previousItem = previousDataMap.get( currentItem.item );
+
+ // If no previous response provided (no comparison), use current data for both periods
+ // This matches the behavior of the existing API when compare_from/to equal from/to
+ const previousValue = previousItem?.value || currentItem.value;
+ const previousIntervals = previousItem?.intervals || currentItem.intervals;
+
+ return {
+ item: currentItem.item,
+ current_period: {
+ value: currentItem.value,
+ intervals: currentItem.intervals,
+ },
+ previous_period: {
+ value: previousValue,
+ intervals: previousIntervals,
+ },
+ };
+ } );
+
+ // Handle items that exist in previous period but not in current
+ // This ensures we don't lose data when an item had sales in the previous period but not current
+ if ( previousResponse ) {
+ previousResponse.data.forEach( previousItem => {
+ const existsInCurrent = currentResponse.data.some( item => item.item === previousItem.item );
+
+ if ( ! existsInCurrent ) {
+ normalizedData.push( {
+ item: previousItem.item,
+ current_period: {
+ value: '0',
+ intervals: previousItem.intervals.map( interval => ( {
+ ...interval,
+ net_sales: '0',
+ } ) ),
+ },
+ previous_period: {
+ value: previousItem.value,
+ intervals: previousItem.intervals,
+ },
+ } );
+ }
+ } );
+ }
+
+ return {
+ view: currentResponse.view,
+ order_by: currentResponse.order_by,
+ data: normalizedData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts
new file mode 100644
index 000000000000..09572282b098
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts
@@ -0,0 +1,118 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportOrderAttributionSummary } from '../../api/report-order-attribution-summary-fetch';
+import { sanitizeStringNumber } from '../utils';
+
+type OrderAttributionSummaryResponse = Awaited<
+ ReturnType< typeof fetchReportOrderAttributionSummary >
+>;
+
+type OrderAttributionView = OrderAttributionSummaryResponse[ 'view' ];
+
+/**
+ * Internal types for processing
+ */
+type OrderAttributionInterval = {
+ time_interval: string;
+ date_start: string;
+ date_end: string;
+ net_sales: string;
+};
+
+type OrderAttributionPeriod = {
+ value: string;
+ intervals: OrderAttributionInterval[];
+};
+
+type OrderAttributionSummaryItem = {
+ item: string;
+ current_period: OrderAttributionPeriod;
+ previous_period: OrderAttributionPeriod;
+};
+
+/**
+ * Processed (sanitized) response types
+ */
+type SanitizedOrderAttributionInterval = {
+ time_interval: string;
+ date_start: string;
+ date_end: string;
+ net_sales: number;
+};
+
+type SanitizedOrderAttributionPeriod = {
+ value: number;
+ intervals: SanitizedOrderAttributionInterval[];
+};
+
+type SanitizedOrderAttributionSummaryItem = {
+ item: string;
+ current_period: SanitizedOrderAttributionPeriod;
+ previous_period: SanitizedOrderAttributionPeriod;
+};
+
+export type SanitizedOrderAttributionSummaryResponse = {
+ view: OrderAttributionView;
+ order_by: string;
+ data: SanitizedOrderAttributionSummaryItem[];
+};
+
+/**
+ * Sanitizes a single interval by converting string net_sales to number
+ * @param interval
+ */
+function sanitizeOrderAttributionInterval(
+ interval: OrderAttributionInterval
+): SanitizedOrderAttributionInterval {
+ return {
+ time_interval: interval.time_interval,
+ date_start: interval.date_start,
+ date_end: interval.date_end,
+ net_sales: sanitizeStringNumber( interval.net_sales ),
+ };
+}
+
+/**
+ * Sanitizes a period by converting value to number and intervals
+ * @param period
+ */
+function sanitizeOrderAttributionPeriod(
+ period: OrderAttributionPeriod
+): SanitizedOrderAttributionPeriod {
+ return {
+ value: sanitizeStringNumber( period.value ),
+ intervals: period.intervals.map( sanitizeOrderAttributionInterval ),
+ };
+}
+
+/**
+ * Sanitizes a single order attribution summary item
+ * @param item
+ */
+function sanitizeOrderAttributionSummaryItem(
+ item: OrderAttributionSummaryItem
+): SanitizedOrderAttributionSummaryItem {
+ return {
+ item: item.item,
+ current_period: sanitizeOrderAttributionPeriod( item.current_period ),
+ previous_period: sanitizeOrderAttributionPeriod( item.previous_period ),
+ };
+}
+
+/**
+ * Sanitizes the order attribution summary response by converting all string
+ * numbers to actual numbers
+ *
+ * @param response - Raw API response from /summary endpoint
+ * @return Sanitized response with numbers instead of strings
+ */
+export function sanitizeReportOrderAttributionSummaryResponse(
+ response: OrderAttributionSummaryResponse
+): SanitizedOrderAttributionSummaryResponse {
+ return {
+ view: response.view,
+ order_by: response.order_by,
+ data: response.data.map( sanitizeOrderAttributionSummaryItem ),
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts
new file mode 100644
index 000000000000..75be705212bb
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts
@@ -0,0 +1,84 @@
+/**
+ * Internal dependencies
+ */
+import { safeParseFloat, safeParseInt } from '../../utils/parsing';
+import type {
+ ReportsOrdersByDateResponse,
+ RequestReportOrdersParams,
+} from '../../api/report-orders-fetch';
+import type { Override } from '../../utils/types';
+
+/**
+ * Re-export the request params type for backwards compatibility.
+ * The orders-by-product-type endpoint uses the same request/response
+ * types as the orders endpoint.
+ */
+export type { RequestReportOrdersParams as RequestReportOrdersByProductTypeParams };
+
+type ReportsOrdersByProductTypeByDateResponse = ReportsOrdersByDateResponse;
+type RawOrdersByProductTypeReportDataItem =
+ ReportsOrdersByProductTypeByDateResponse[ 'data' ][ number ];
+type SanitizedOrdersByProductTypeByDateItem = Override<
+ RawOrdersByProductTypeReportDataItem,
+ {
+ average_order_value: number;
+ avg_items: number;
+ cogs_amount: number;
+ coupons: number;
+ orders_no: number;
+ orders_value_gross: number;
+ orders_value_net: number;
+ product_net_revenue: number;
+ profit_margin: number;
+ refunds: number;
+ total_sales: number;
+ }
+>;
+
+/**
+ * Sanitize/process a single orders by product type item by converting strings to numbers
+ * @param item
+ */
+function sanitizeOrdersByProductTypeItem(
+ item: RawOrdersByProductTypeReportDataItem
+): SanitizedOrdersByProductTypeByDateItem {
+ return {
+ ...item,
+ average_order_value: safeParseFloat( item.average_order_value ),
+ avg_items: safeParseFloat( item.avg_items ),
+ cogs_amount: safeParseFloat( item.cogs_amount ),
+ coupons: safeParseInt( item.coupons ),
+ orders_no: safeParseInt( item.orders_no ),
+ orders_value_gross: safeParseFloat( item.orders_value_gross ),
+ orders_value_net: safeParseFloat( item.orders_value_net ),
+ product_net_revenue: safeParseFloat( item.product_net_revenue ),
+ profit_margin: safeParseFloat( item.profit_margin ),
+ refunds: safeParseFloat( item.refunds ),
+ total_sales: safeParseFloat( item.total_sales ),
+ };
+}
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedOrdersByProductTypeByDateResponse = {
+ summary: SanitizedOrdersByProductTypeByDateItem;
+ data: SanitizedOrdersByProductTypeByDateItem[];
+};
+
+/**
+ * Sanitize the response from the reports/orders-by-product-type/by-date endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ *
+ * The `summary` single item has basically the same structure
+ * as the `data` array items, so we can use the same mapper function for both.
+ * @param response
+ */
+export const sanitizeReportOrdersByProductTypeResponse = (
+ response: ReportsOrdersByProductTypeByDateResponse
+): SanitizedOrdersByProductTypeByDateResponse => {
+ return {
+ summary: sanitizeOrdersByProductTypeItem( response.summary ),
+ data: response.data.map( sanitizeOrdersByProductTypeItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts
new file mode 100644
index 000000000000..00b7a8acf6d4
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts
@@ -0,0 +1,79 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportOrders } from '../../api/report-orders-fetch';
+import { safeParseFloat, safeParseInt } from '../../utils/parsing';
+import type { Override } from '../../utils/types';
+
+type ReportsOrdersByDateResponse = Awaited< ReturnType< typeof fetchReportOrders > >;
+type RawOrdersReportDataItem = ReportsOrdersByDateResponse[ 'data' ][ number ];
+type SanitizedOrdersByDateItem = Override<
+ RawOrdersReportDataItem,
+ {
+ average_order_value: number;
+ avg_items: number;
+ cogs_amount: number;
+ coupons: number;
+ orders_no: number;
+ orders_value_gross: number;
+ orders_value_net: number;
+ paid_orders_count: number;
+ paid_net_sales: number;
+ product_net_revenue: number;
+ profit_margin: number;
+ refunds: number;
+ total_sales: number;
+ unpaid_orders_count: number;
+ unpaid_net_sales: number;
+ }
+>;
+
+/**
+ * Sanitize/process a single order item by converting strings to numbers
+ * @param item
+ */
+function sanitizeOrderItem( item: RawOrdersReportDataItem ): SanitizedOrdersByDateItem {
+ return {
+ ...item,
+ average_order_value: safeParseFloat( item.average_order_value ),
+ avg_items: safeParseFloat( item.avg_items ),
+ cogs_amount: safeParseFloat( item.cogs_amount ),
+ coupons: safeParseInt( item.coupons ),
+ orders_no: safeParseInt( item.orders_no ),
+ orders_value_gross: safeParseFloat( item.orders_value_gross ),
+ orders_value_net: safeParseFloat( item.orders_value_net ),
+ paid_orders_count: safeParseInt( item.paid_orders_count ),
+ paid_net_sales: safeParseFloat( item.paid_net_sales ),
+ product_net_revenue: safeParseFloat( item.product_net_revenue ),
+ profit_margin: safeParseFloat( item.profit_margin ),
+ refunds: safeParseFloat( item.refunds ),
+ total_sales: safeParseFloat( item.total_sales ),
+ unpaid_orders_count: safeParseInt( item.unpaid_orders_count ),
+ unpaid_net_sales: safeParseFloat( item.unpaid_net_sales ),
+ };
+}
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedOrdersByDateResponse = {
+ summary: SanitizedOrdersByDateItem;
+ data: SanitizedOrdersByDateItem[];
+};
+
+/**
+ * Sanitize the response from the reports/orders/by-date endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ *
+ * The `summary` single item has basically the same structure
+ * as the `data` array items, so we can use the same mapper function for both.
+ * @param response
+ */
+export const sanitizeReportOrdersResponse = (
+ response: ReportsOrdersByDateResponse
+): SanitizedOrdersByDateResponse => {
+ return {
+ summary: sanitizeOrderItem( response.summary ),
+ data: response.data.map( sanitizeOrderItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts
new file mode 100644
index 000000000000..6d2ba237964f
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts
@@ -0,0 +1,83 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportProducts } from '../../api/report-products-fetch';
+import type { Override } from '../../utils/types';
+
+type ReportProductsResponse = Awaited< ReturnType< typeof fetchReportProducts > >;
+
+type RawProductsReportDataItem = ReportProductsResponse[ 'data' ][ number ];
+type RawProductsReportSummary = ReportProductsResponse[ 'summary' ];
+
+type SanitizedProductsItem = Override<
+ RawProductsReportDataItem,
+ {
+ product_id: number;
+ orders_count: number;
+ product_net_revenue: number;
+ total_quantity: number;
+ }
+>;
+
+type SanitizedProductsSummary = Override<
+ RawProductsReportSummary,
+ {
+ total_orders: number;
+ total_products: number;
+ total_quantity: number;
+ total_revenue: number;
+ }
+>;
+
+/**
+ * Sanitize/process a single product item by converting strings to numbers
+ * @param item
+ */
+function sanitizeProductItem( item: RawProductsReportDataItem ): SanitizedProductsItem {
+ return {
+ ...item,
+ product_id: parseInt( item.product_id, 10 ),
+ orders_count: parseInt( item.orders_count, 10 ),
+ product_net_revenue: parseFloat( item.product_net_revenue ),
+ total_quantity: parseInt( item.total_quantity, 10 ),
+ };
+}
+
+/**
+ *
+ * @param summary
+ */
+function sanitizeProductSummary( summary: RawProductsReportSummary ): SanitizedProductsSummary {
+ return {
+ ...summary,
+ total_orders: parseInt( summary.total_orders, 10 ),
+ total_products: parseInt( summary.total_products, 10 ),
+ total_quantity: parseInt( summary.total_quantity, 10 ),
+ total_revenue: parseFloat( summary.total_revenue ),
+ };
+}
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedProductsResponse = {
+ summary: SanitizedProductsSummary;
+ data: SanitizedProductsItem[];
+};
+
+/**
+ * Sanitize the response from the reports/products endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ *
+ * The `summary` single item has basically the same structure
+ * as the `data` array items, so we can use the same mapper function for both.
+ * @param response
+ */
+export const sanitizeReportProductsResponse = (
+ response: ReportProductsResponse
+): SanitizedProductsResponse => {
+ return {
+ summary: sanitizeProductSummary( response.summary ),
+ data: ( response.data || [] ).map( sanitizeProductItem ),
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts
new file mode 100644
index 000000000000..127c39843e31
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts
@@ -0,0 +1,75 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportSessionsByDevice } from '../../api/report-sessions-by-device-fetch';
+
+/**
+ * Inferred types from fetch response
+ */
+type ReportsSessionsByDeviceResponse = Awaited< ReturnType< typeof fetchReportSessionsByDevice > >;
+
+/**
+ * Raw item type from API response
+ */
+type SessionsByDeviceItem = ReportsSessionsByDeviceResponse[ 'data' ][ number ];
+
+/**
+ * Sanitized item with numeric values
+ */
+type SanitizedSessionsByDeviceItem = {
+ device_type: string;
+ active_sessions: number;
+};
+
+/**
+ * Summary with total sessions
+ */
+type SessionsByDeviceSummary = {
+ total_sessions: number;
+};
+
+/**
+ * Processed response structure
+ */
+type SanitizedSessionsByDeviceResponse = {
+ summary: SessionsByDeviceSummary;
+ data: SanitizedSessionsByDeviceItem[];
+};
+
+/**
+ * Sanitize a single sessions by device item by converting strings to numbers.
+ *
+ * @param item - Raw item from API response
+ */
+function sanitizeSessionsByDeviceItem( item: SessionsByDeviceItem ): SanitizedSessionsByDeviceItem {
+ return {
+ device_type: item.device_type || '',
+ active_sessions: parseInt( item.active_sessions, 10 ) || 0,
+ };
+}
+
+/**
+ * Sanitize the response from the sessions/by-device endpoint.
+ *
+ * Converts string values to numbers for easier calculations and charting.
+ * Also calculates total sessions summary.
+ *
+ * @param response - Raw API response with summary and items
+ */
+export const sanitizeReportSessionsByDeviceResponse = (
+ response: ReportsSessionsByDeviceResponse
+): SanitizedSessionsByDeviceResponse => {
+ const items = response?.data ?? [];
+ const data = items
+ .filter( item => item.device_type ) // Filter out empty device types
+ .map( sanitizeSessionsByDeviceItem );
+
+ const totalSessions = data.reduce( ( acc, item ) => acc + item.active_sessions, 0 );
+
+ return {
+ summary: {
+ total_sessions: totalSessions,
+ },
+ data,
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/utils.ts b/projects/packages/premium-analytics/packages/data/src/processing/utils.ts
new file mode 100644
index 000000000000..c0dc5636992a
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/utils.ts
@@ -0,0 +1,11 @@
+/**
+ * Converts a string number to an actual number, with fallback to 0
+ * for invalid values.
+ *
+ * @param value - String number from API
+ * @return Parsed number or 0 if invalid
+ */
+export function sanitizeStringNumber( value: string ): number {
+ const parsed = parseFloat( value );
+ return isNaN( parsed ) ? 0 : parsed;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts
new file mode 100644
index 000000000000..b9dac4936c16
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts
@@ -0,0 +1,78 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportVisitorsByLocation } from '../../api/report-visitors-by-location-fetch';
+import type { Override } from '../../utils/types';
+
+/**
+ * Inferred types
+ */
+type ReportsVisitorsByLocationResponse = Awaited<
+ ReturnType< typeof fetchReportVisitorsByLocation >
+>;
+type RawVisitorsByLocationItem = ReportsVisitorsByLocationResponse[ 'data' ][ number ];
+type RawVisitorsByLocationSummary = NonNullable< ReportsVisitorsByLocationResponse[ 'summary' ] >;
+
+type SanitizedVisitorsByLocationItem = Override<
+ RawVisitorsByLocationItem,
+ {
+ visitors: number;
+ }
+>;
+
+type SanitizedVisitorsByLocationSummary = Override<
+ RawVisitorsByLocationSummary,
+ {
+ visitors: number;
+ }
+>;
+
+/**
+ *
+ * @param item
+ */
+function sanitizeVisitorsByLocationItem(
+ item: RawVisitorsByLocationItem
+): SanitizedVisitorsByLocationItem {
+ const visitors = Number.parseInt( item.visitors, 10 );
+
+ return {
+ ...item,
+ visitors: Number.isNaN( visitors ) ? 0 : visitors,
+ };
+}
+
+/**
+ *
+ * @param summary
+ */
+function sanitizeVisitorsByLocationSummary(
+ summary: RawVisitorsByLocationSummary
+): SanitizedVisitorsByLocationSummary {
+ const visitors = Number.parseInt( summary.visitors, 10 );
+
+ return {
+ ...summary,
+ visitors: Number.isNaN( visitors ) ? 0 : visitors,
+ };
+}
+
+type SanitizedVisitorsByLocationResponse = {
+ summary: SanitizedVisitorsByLocationSummary;
+ data: SanitizedVisitorsByLocationItem[];
+};
+
+export const sanitizeReportVisitorsByLocationResponse = (
+ response: ReportsVisitorsByLocationResponse
+): SanitizedVisitorsByLocationResponse => {
+ const defaultSummary: RawVisitorsByLocationSummary = {
+ visitors: '0',
+ date_start: '',
+ date_end: '',
+ };
+
+ return {
+ summary: sanitizeVisitorsByLocationSummary( response?.summary ?? defaultSummary ),
+ data: response?.data ? response.data.map( sanitizeVisitorsByLocationItem ) : [],
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts
new file mode 100644
index 000000000000..7d14cf71ebc1
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts
@@ -0,0 +1,80 @@
+/**
+ * Internal dependencies
+ */
+import { fetchReportVisitors } from '../../api/report-visitors-fetch';
+import type { Override } from '../../utils/types';
+
+/**
+ * Inferred types
+ */
+type ReportsVisitorsByDateResponse = Awaited< ReturnType< typeof fetchReportVisitors > >;
+type RawVisitorsReportDataItem = ReportsVisitorsByDateResponse[ 'data' ][ number ];
+type RawVisitorsReportDataSummary = ReportsVisitorsByDateResponse[ 'summary' ];
+
+type SanitizedVisitorsByDateItem = Override<
+ RawVisitorsReportDataItem,
+ {
+ active_sessions: number;
+ visitors: number;
+ time_interval?: string;
+ }
+>;
+
+type SanitizedVisitorsByDateSummary = Override<
+ RawVisitorsReportDataSummary,
+ {
+ active_sessions: number;
+ visitors: number;
+ }
+>;
+
+type SanitizeVisitorsItemArg = Override<
+ RawVisitorsReportDataItem,
+ {
+ time_interval?: string;
+ }
+>;
+
+/**
+ * Sanitize/process a single visitors item by converting strings to numbers
+ * @param item
+ */
+function sanitizeVisitorsItem( item: SanitizeVisitorsItemArg ): SanitizedVisitorsByDateItem {
+ return {
+ ...item,
+ active_sessions: parseInt( item.active_sessions, 10 ),
+ visitors: parseInt( item.visitors, 10 ),
+ };
+}
+
+/**
+ * Processed response with numeric values
+ */
+type SanitizedVisitorsByDateResponse = {
+ summary: SanitizedVisitorsByDateSummary;
+ data: SanitizedVisitorsByDateItem[];
+};
+
+/**
+ * Sanitize the response from the sessions/by-date endpoint
+ * Converts string values to numbers for easier calculations and charting.
+ *
+ * The `summary` single item has basically the same structure
+ * as the `data` array items, so we can use the same mapper function for both.
+ * @param response
+ */
+export const sanitizeReportVisitorsResponse = (
+ response: ReportsVisitorsByDateResponse
+): SanitizedVisitorsByDateResponse => {
+ const defaultSummary = {
+ active_sessions: '0',
+ visitors: '0',
+ date_start: '',
+ date_end: '',
+ };
+
+ return {
+ summary: sanitizeVisitorsItem( response?.summary ?? defaultSummary ),
+ data: response?.data ? response.data.map( sanitizeVisitorsItem ) : [],
+ };
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx
new file mode 100644
index 000000000000..cf1058f6255e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx
@@ -0,0 +1,108 @@
+/**
+ * External dependencies
+ */
+import { onlineManager } from '@tanstack/react-query';
+import {
+ createContext,
+ useContext,
+ useEffect,
+ useMemo,
+ useSyncExternalStore,
+ type ReactNode,
+} from 'react';
+/**
+ * Internal dependencies
+ */
+import { globalErrorManager, type GlobalErrorType } from './global-error-manager';
+
+interface GlobalErrorContextValue {
+ globalError: GlobalErrorType;
+ setGlobalError: ( error: GlobalErrorType ) => void;
+ clearGlobalError: () => void;
+ isGlobalError: boolean;
+}
+
+const GlobalErrorContext = createContext< GlobalErrorContextValue | null >( null );
+
+/**
+ * Connects React to the global error manager via useSyncExternalStore.
+ * Also subscribes to network status changes via onlineManager.
+ * @param root0
+ * @param root0.children
+ */
+export function GlobalErrorProvider( { children }: { children: ReactNode } ) {
+ const globalError = useSyncExternalStore(
+ globalErrorManager.subscribe,
+ globalErrorManager.getError,
+ globalErrorManager.getError
+ );
+
+ /**
+ * Subscribe to TanStack Query's onlineManager to detect network status.
+ *
+ * When offline, TanStack Query pauses queries (doesn't execute them),
+ * so QueryCache.onError never fires. We detect offline status here and
+ * properly clean up the subscription when the provider unmounts.
+ */
+ useEffect( () => {
+ // Check initial online status on mount
+ if ( ! onlineManager.isOnline() ) {
+ globalErrorManager.setError( 'network' );
+ }
+
+ const unsubscribe = onlineManager.subscribe( isOnline => {
+ if ( ! isOnline ) {
+ globalErrorManager.setError( 'network' );
+ } else if ( globalErrorManager.getError() === 'network' ) {
+ globalErrorManager.clearError();
+ }
+ } );
+
+ return unsubscribe;
+ }, [] );
+
+ const contextValue = useMemo(
+ () => ( {
+ globalError,
+ setGlobalError: globalErrorManager.setError,
+ clearGlobalError: globalErrorManager.clearError,
+ isGlobalError: globalError !== null,
+ } ),
+ [ globalError ]
+ );
+
+ return (
+ { children }
+ );
+}
+
+let hasWarnedAboutMissingProvider = false;
+
+const defaultContextValue: GlobalErrorContextValue = {
+ globalError: null,
+ setGlobalError: () => {},
+ clearGlobalError: () => {},
+ isGlobalError: false,
+};
+
+/**
+ * Access global error state. Returns defaults if used outside GlobalErrorProvider.
+ */
+export function useGlobalError(): GlobalErrorContextValue {
+ const context = useContext( GlobalErrorContext );
+
+ if ( context ) {
+ return context;
+ }
+
+ if ( ! hasWarnedAboutMissingProvider ) {
+ hasWarnedAboutMissingProvider = true;
+ // eslint-disable-next-line no-console
+ console.warn(
+ 'useGlobalError was called outside of GlobalErrorProvider. ' +
+ 'Wrap your component tree with GlobalErrorProvider.'
+ );
+ }
+
+ return defaultContextValue;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts b/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts
new file mode 100644
index 000000000000..9686d09e595b
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts
@@ -0,0 +1,35 @@
+/**
+ * External dependencies
+ */
+
+export type GlobalErrorType = 'network' | 'auth' | 'server' | null;
+
+type Listener = () => void;
+
+/**
+ * Manages global error state outside of React, enabling error state to be set
+ * from onlineManager subscription and consumed via useSyncExternalStore.
+ */
+class GlobalErrorManager {
+ private error: GlobalErrorType = null;
+ private listeners = new Set< Listener >();
+
+ getError = (): GlobalErrorType => this.error;
+
+ setError = ( error: GlobalErrorType ): void => {
+ if ( this.error === error ) {
+ return;
+ }
+ this.error = error;
+ this.listeners.forEach( listener => listener() );
+ };
+
+ clearError = (): void => this.setError( null );
+
+ subscribe = ( listener: Listener ): ( () => void ) => {
+ this.listeners.add( listener );
+ return () => this.listeners.delete( listener );
+ };
+}
+
+export const globalErrorManager = new GlobalErrorManager();
diff --git a/projects/packages/premium-analytics/packages/data/src/providers/index.ts b/projects/packages/premium-analytics/packages/data/src/providers/index.ts
new file mode 100644
index 000000000000..9b6bc769e9bf
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/providers/index.ts
@@ -0,0 +1,5 @@
+export { queryClient, AnalyticsQueryClientProvider } from './query-client-provider';
+
+export { GlobalErrorProvider, useGlobalError } from './global-error-context';
+
+export { globalErrorManager, type GlobalErrorType } from './global-error-manager';
diff --git a/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx
new file mode 100644
index 000000000000..1baf4f0ea5be
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx
@@ -0,0 +1,162 @@
+/**
+ * External dependencies
+ */
+import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query';
+import { ReactNode, lazy, Suspense } from 'react';
+/**
+ * Internal dependencies
+ */
+import { globalErrorManager } from './global-error-manager';
+
+const DEFAULT_STALE_TIME = 5 * 60 * 1000;
+const DEFAULT_GC_TIME = 10 * 60 * 1000;
+
+/**
+ * Whether to render the React Query Devtools.
+ *
+ * Devtools are opt-in and OFF by default. Enable them by setting a global
+ * debug flag on `window` (e.g. in the browser console:
+ * `window.jetpackPremiumAnalyticsQueryDevtools = true`) and reloading. They
+ * are also enabled automatically outside of production builds.
+ *
+ * @return Whether the devtools should be rendered.
+ */
+function areQueryDevtoolsEnabled(): boolean {
+ if (
+ typeof window !== 'undefined' &&
+ ( window as { jetpackPremiumAnalyticsQueryDevtools?: boolean } )
+ .jetpackPremiumAnalyticsQueryDevtools === true
+ ) {
+ return true;
+ }
+
+ return process.env.NODE_ENV !== 'production';
+}
+
+const ReactQueryDevtoolsProduction = lazy( () =>
+ import( '@tanstack/react-query-devtools/production' ).then( d => ( {
+ default: d.ReactQueryDevtools,
+ } ) )
+);
+
+/**
+ * Extract HTTP status code from various error formats.
+ * WordPress REST API errors may have different shapes.
+ * @param error
+ */
+function getErrorStatus( error: unknown ): number | null {
+ if ( ! error || typeof error !== 'object' ) {
+ return null;
+ }
+
+ const err = error as Record< string, unknown >;
+
+ // Standard fetch Response error
+ if ( typeof err.status === 'number' ) {
+ return err.status;
+ }
+
+ // WordPress REST API error format
+ if ( err.data && typeof err.data === 'object' ) {
+ const data = err.data as Record< string, unknown >;
+ if ( typeof data.status === 'number' ) {
+ return data.status;
+ }
+ }
+
+ // Nested response object
+ if ( err.response && typeof err.response === 'object' ) {
+ const response = err.response as Record< string, unknown >;
+ if ( typeof response.status === 'number' ) {
+ return response.status;
+ }
+ }
+
+ return null;
+}
+
+/**
+ * QueryCache with global error detection for auth and server errors.
+ *
+ * Error codes handled:
+ * - 401: Authentication failure (session expired, invalid token)
+ * - 502: Bad gateway (proxy/load balancer can't reach upstream)
+ * - 503: Service unavailable (server overloaded or under maintenance)
+ * - 504: Gateway timeout (request took too long)
+ *
+ * This is QueryClient configuration (not a side effect subscription), so it's
+ * appropriate at module level. The globalErrorManager singleton is used here
+ * because QueryClient must be instantiated once (singleton pattern), but the
+ * error state is safely consumed via useSyncExternalStore in GlobalErrorProvider.
+ *
+ * Network errors are handled separately in GlobalErrorProvider via onlineManager.
+ */
+const queryCache = new QueryCache( {
+ onError: error => {
+ const currentError = globalErrorManager.getError();
+
+ // Don't override network error (highest priority)
+ if ( currentError === 'network' ) {
+ return;
+ }
+
+ const status = getErrorStatus( error );
+
+ if ( status === 401 ) {
+ // Auth errors take precedence over server errors, but not network errors.
+ if ( currentError !== 'auth' ) {
+ globalErrorManager.setError( 'auth' );
+ }
+ } else if ( status === 502 || status === 503 || status === 504 ) {
+ // Server errors: only set if no higher-priority error exists.
+ if ( currentError !== 'auth' && currentError !== 'server' ) {
+ globalErrorManager.setError( 'server' );
+ }
+ }
+ },
+ onSuccess: () => {
+ // Clear transient server errors once queries start succeeding again.
+ if ( globalErrorManager.getError() === 'server' ) {
+ globalErrorManager.clearError();
+ }
+ },
+} );
+
+export const queryClient = new QueryClient( {
+ queryCache,
+ defaultOptions: {
+ queries: {
+ /*
+ * Stale time is the time after which the data
+ * is considered stale and a new request is made.
+ * Stale time: 5 minutes
+ */
+ staleTime: DEFAULT_STALE_TIME,
+
+ /*
+ * GC time is the time after which the data is considered garbage
+ * collected and removed from the cache.
+ * GC time: 10 minutes
+ */
+ gcTime: DEFAULT_GC_TIME,
+
+ /**
+ * Noop fetcher to prevent react-query errors for empty queries in console.
+ */
+ queryFn: () => Promise.resolve( undefined ),
+ },
+ },
+} );
+
+export const AnalyticsQueryClientProvider = ( { children }: { children: ReactNode } ) => {
+ return (
+
+ <>{ children }>
+ { areQueryDevtoolsEnabled() && (
+
+
+
+ ) }
+
+ );
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/index.ts b/projects/packages/premium-analytics/packages/data/src/queries/index.ts
new file mode 100644
index 000000000000..f5b0604ab3f9
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/index.ts
@@ -0,0 +1,12 @@
+export { reportOrdersQuery } from './report-orders-query';
+export { reportOrderAttributionSummaryQuery } from './report-order-attribution-summary-query';
+export { reportCouponsQuery } from './report-coupons-query';
+export { reportCouponsByDateQuery } from './report-coupons-by-date-query';
+export { reportCustomersQuery } from './report-customers-query';
+export { reportCustomersByDateQuery } from './report-customers-by-date-query';
+export { reportConversionRateQuery } from './report-conversion-rate-query';
+export { reportProductsQuery } from './report-products-query';
+export { reportVisitorsQuery } from './report-visitors-query';
+export { reportVisitorsByLocationQuery } from './report-visitors-by-location-query';
+export { reportSessionsByDeviceQuery } from './report-sessions-by-device-query';
+export { reportBookingsQuery } from './report-bookings-query';
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts
new file mode 100644
index 000000000000..8a223b3f8ce9
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportBookings } from '../api';
+import { sanitizeReportBookingsResponse } from '../processing/bookings';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportBookingsParams = Parameters< typeof fetchReportBookings >[ 0 ];
+
+const getReportBookingsQueryKey = ( p: RequestReportBookingsParams ) =>
+ [ 'reports', 'bookings', 'by-date', p.from, p.to, p.interval, p.date_type, p.filters ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportBookingsQuery(
+ params: RequestReportBookingsParams
+): UseQueryOptions< ReportDataMap[ 'bookings' ] > {
+ return {
+ queryKey: getReportBookingsQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportBookings( params );
+ return sanitizeReportBookingsResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts
new file mode 100644
index 000000000000..7e60af58f333
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts
@@ -0,0 +1,41 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportConversionRate } from '../api/report-conversion-rate-fetch';
+import { sanitizeReportConversionRateResponse } from '../processing/conversion-rate';
+import type { RequestReportConversionRateParams } from '../api/report-conversion-rate-fetch';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+const getReportConversionRateQueryKey = ( p: RequestReportConversionRateParams ) =>
+ [ 'reports', 'conversion-rate', p.from, p.to, p.interval, p.date_type, p.filters ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportConversionRateQuery(
+ params: RequestReportConversionRateParams
+): UseQueryOptions< ReturnType< typeof sanitizeReportConversionRateResponse > > {
+ return {
+ queryKey: getReportConversionRateQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportConversionRate( params );
+ return sanitizeReportConversionRateResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts
new file mode 100644
index 000000000000..a02418ab87e1
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportCouponsByDate } from '../api';
+import { sanitizeReportCouponsByDateResponse } from '../processing/coupons-by-date';
+import { FilterCondition } from '../types/filter-condition';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportCouponsByDateParams = Parameters< typeof fetchReportCouponsByDate >[ 0 ] & {
+ filters?: FilterCondition[];
+};
+
+const getQueryKey = ( p: RequestReportCouponsByDateParams ) =>
+ [ 'reports', 'couponsByDate', p.from, p.to, p.interval, p.date_type, p.filters ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportCouponsByDateQuery(
+ params: RequestReportCouponsByDateParams
+): UseQueryOptions< ReportDataMap[ 'couponsByDate' ] > {
+ return {
+ queryKey: getQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportCouponsByDate( params );
+ return sanitizeReportCouponsByDateResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts
new file mode 100644
index 000000000000..c37f0d78ec0d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts
@@ -0,0 +1,46 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportCoupons } from '../api';
+import { sanitizeReportCouponsResponse } from '../processing/coupons';
+import { FilterCondition } from '../types/filter-condition';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportCouponsParams = Parameters< typeof fetchReportCoupons >[ 0 ] & {
+ filters?: FilterCondition[];
+};
+
+const getReportCouponsQueryKey = ( p: RequestReportCouponsParams ) =>
+ [ 'reports', 'coupons', p.from, p.to, p.interval, p.date_type, p.filters ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportCouponsQuery(
+ params: RequestReportCouponsParams
+): UseQueryOptions< ReportDataMap[ 'coupons' ] > {
+ return {
+ queryKey: getReportCouponsQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportCoupons( params );
+ return sanitizeReportCouponsResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts
new file mode 100644
index 000000000000..1537d84c7353
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportCustomersByDate } from '../api/report-customers-by-date-fetch';
+import { sanitizeReportCustomersByDateResponse } from '../processing/customers-by-date';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportCustomersByDateParams = Parameters< typeof fetchReportCustomersByDate >[ 0 ];
+
+const getReportCustomersByDateQueryKey = ( p: RequestReportCustomersByDateParams ) =>
+ [ 'reports', 'customers', 'by-date', p.from, p.to, p.interval, p.date_type ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportCustomersByDateQuery(
+ params: RequestReportCustomersByDateParams
+): UseQueryOptions< ReportDataMap[ 'customersByDate' ] > {
+ return {
+ queryKey: getReportCustomersByDateQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportCustomersByDate( params );
+ return sanitizeReportCustomersByDateResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts
new file mode 100644
index 000000000000..0cfdb1a30cfa
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts
@@ -0,0 +1,51 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportCustomers } from '../api';
+import { sanitizeReportCustomersResponse } from '../processing/customers';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportCustomersParams = Parameters< typeof fetchReportCustomers >[ 0 ];
+
+const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [
+ 'reports',
+ 'customers',
+ 'new-returning',
+ p.from,
+ p.to,
+ p.date_type,
+ p.filters,
+];
+
+/**
+ *
+ * @param params
+ */
+export function reportCustomersQuery(
+ params: RequestReportCustomersParams
+): UseQueryOptions< ReportDataMap[ 'customers' ] > {
+ return {
+ queryKey: getReportCustomersQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportCustomers( params );
+ return sanitizeReportCustomersResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from and to are set.
+ * Note: interval is not required for customers endpoint.
+ */
+ enabled: !! ( params.from && params.to ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts
new file mode 100644
index 000000000000..55be6f0e756d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts
@@ -0,0 +1,130 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportOrderAttributionSummary, fetchReportOrderAttributionByProduct } from '../api';
+import {
+ sanitizeReportOrderAttributionSummaryResponse,
+ normalizeOrderAttributionByProductResponse,
+ type SanitizedOrderAttributionSummaryResponse,
+} from '../processing/order-attribution';
+import { hasProductFilters } from '../utils/product-filters';
+import type { FilterCondition } from '../types/filter-condition';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type ReportOrderAttributionSummaryParams = Parameters<
+ typeof fetchReportOrderAttributionSummary
+>[ 0 ] & {
+ filters?: FilterCondition[];
+};
+
+/**
+ * Creates a query key for order attribution queries.
+ *
+ * Note: All comparison parameters are included in the query key because
+ * order attribution returns both primary and comparison data in a single response.
+ * @param params
+ */
+const getReportOrderAttributionQueryKey = ( params: ReportOrderAttributionSummaryParams ) =>
+ [
+ 'reports',
+ 'order-attribution',
+ params.view,
+ params.from,
+ params.to,
+ params.interval,
+ params.date_type,
+ params.compare_from,
+ params.compare_to,
+ params.filters,
+ ] as const;
+
+/**
+ * React Query configuration for order attribution summary data.
+ *
+ * This query is designed to be used with `use-report` hook, which provides
+ * standardized loading states and comparison handling.
+ *
+ * Important architectural notes:
+ * - Unlike other report queries, order attribution includes comparison data in the
+ * PRIMARY response, not in a separate comparison query
+ * - When used with `use-report`, the comparison query is disabled (it's a no-op)
+ * - This query supports two API endpoints:
+ * 1. Regular order-attribution API: Returns both periods in a single response
+ * 2. By-product API: Fetches periods separately, then normalizes to match (1)
+ *
+ * @param params - Query parameters including date ranges and optional filters
+ * @return React Query options with query key, fetch function, and enabled state
+ */
+export function reportOrderAttributionSummaryQuery(
+ params: ReportOrderAttributionSummaryParams
+): UseQueryOptions< SanitizedOrderAttributionSummaryResponse > {
+ return {
+ queryKey: getReportOrderAttributionQueryKey( params ),
+ queryFn: async () => {
+ const hasProductFiltersValue = hasProductFilters( params.filters );
+
+ // Choose API based on whether product filters are present
+ if ( hasProductFiltersValue ) {
+ // By-product API path: Fetch primary and comparison periods in parallel
+ const { compare_from, compare_to } = params;
+
+ // Determine if we need to fetch comparison period
+ const shouldFetchComparison =
+ compare_from &&
+ compare_to &&
+ ( compare_from !== params.from || compare_to !== params.to );
+
+ // Fetch both periods in parallel for better performance
+ const [ currentResponse, previousResponse ] = await Promise.all( [
+ fetchReportOrderAttributionByProduct( {
+ from: params.from,
+ to: params.to,
+ interval: params.interval,
+ view: params.view,
+ filters: params.filters,
+ date_type: params.date_type,
+ } ),
+ shouldFetchComparison
+ ? fetchReportOrderAttributionByProduct( {
+ from: compare_from,
+ to: compare_to,
+ interval: params.interval,
+ view: params.view,
+ filters: params.filters,
+ date_type: params.date_type,
+ } )
+ : Promise.resolve( undefined ),
+ ] );
+
+ // Normalize to match the regular API structure (includes both periods)
+ const normalizedResponse = normalizeOrderAttributionByProductResponse(
+ currentResponse,
+ previousResponse
+ );
+
+ return sanitizeReportOrderAttributionSummaryResponse( normalizedResponse );
+ }
+
+ // Regular API path: Returns both primary and comparison in one response
+ const response = await fetchReportOrderAttributionSummary( params );
+ return sanitizeReportOrderAttributionSummaryResponse( response );
+ },
+
+ /**
+ * Enable the query only when all required parameters are present.
+ * The 'view' parameter is required for order attribution queries.
+ */
+ enabled: !! ( params.from && params.to && params.interval && params.view ),
+
+ /**
+ * Keep previous data while fetching to prevent flash of empty state.
+ * This provides a smoother user experience during data refetching.
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts
new file mode 100644
index 000000000000..29deb2e9a552
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts
@@ -0,0 +1,50 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportOrders } from '../api';
+import { sanitizeReportOrdersResponse } from '../processing/orders';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportOrdersParams = Parameters< typeof fetchReportOrders >[ 0 ];
+
+const getReportOrdersQueryKey = ( p: RequestReportOrdersParams ) => [
+ 'reports',
+ 'orders',
+ p.from,
+ p.to,
+ p.interval,
+ p.date_type,
+ p.filters || [],
+];
+
+/**
+ *
+ * @param params
+ */
+export function reportOrdersQuery(
+ params: RequestReportOrdersParams
+): UseQueryOptions< ReportDataMap[ 'orders' ] > {
+ return {
+ queryKey: getReportOrdersQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportOrders( params );
+ return sanitizeReportOrdersResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts
new file mode 100644
index 000000000000..a46bb686d4ed
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts
@@ -0,0 +1,54 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportProducts } from '../api/report-products-fetch';
+import { sanitizeReportProductsResponse } from '../processing/products';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportProductsParams = Parameters< typeof fetchReportProducts >[ 0 ];
+
+type SanitizedProductsResponse = ReturnType< typeof sanitizeReportProductsResponse >;
+
+const getReportProductsQueryKey = ( p: RequestReportProductsParams ) =>
+ [
+ 'reports',
+ 'products',
+ p.from,
+ p.to,
+ p.date_type,
+ p.limit,
+ p.orderby,
+ p.order,
+ p.filters,
+ ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportProductsQuery(
+ params: RequestReportProductsParams
+): UseQueryOptions< SanitizedProductsResponse > {
+ return {
+ queryKey: getReportProductsQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportProducts( params );
+ return sanitizeReportProductsResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from and to are set.
+ */
+ enabled: !! ( params.from && params.to ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts
new file mode 100644
index 000000000000..b00ce00bf33c
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts
@@ -0,0 +1,45 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportSessionsByDevice } from '../api/report-sessions-by-device-fetch';
+import { sanitizeReportSessionsByDeviceResponse } from '../processing/sessions-by-device';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportSessionsByDeviceParams = Parameters< typeof fetchReportSessionsByDevice >[ 0 ];
+
+const getReportSessionsByDeviceQueryKey = ( p: RequestReportSessionsByDeviceParams ) =>
+ [ 'reports', 'sessions', 'by-device', p.from, p.to ] as const;
+
+/**
+ * Creates query options for fetching sessions by device report data.
+ *
+ * @param params - Request parameters with from/to dates
+ */
+export function reportSessionsByDeviceQuery(
+ params: RequestReportSessionsByDeviceParams
+): UseQueryOptions< ReportDataMap[ 'sessionsByDevice' ] > {
+ return {
+ queryKey: getReportSessionsByDeviceQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportSessionsByDevice( params );
+ return sanitizeReportSessionsByDeviceResponse( response );
+ },
+
+ /**
+ * Enable the query only if from and to dates are set.
+ * Note: This endpoint doesn't use interval (it's not a time-series).
+ */
+ enabled: !! ( params.from && params.to ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts
new file mode 100644
index 000000000000..32663a38b75e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts
@@ -0,0 +1,48 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportVisitorsByLocation } from '../api';
+import { sanitizeReportVisitorsByLocationResponse } from '../processing/visitors-by-location';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportVisitorsByLocationParams = Parameters<
+ typeof fetchReportVisitorsByLocation
+>[ 0 ];
+
+const getReportVisitorsByLocationQueryKey = ( p: RequestReportVisitorsByLocationParams ) =>
+ [
+ 'reports',
+ 'visitors',
+ 'by-location',
+ p.group_by,
+ p.country_code ?? null,
+ p.from,
+ p.to,
+ p.interval,
+ p.limit ?? null,
+ ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportVisitorsByLocationQuery(
+ params: RequestReportVisitorsByLocationParams
+): UseQueryOptions< ReportDataMap[ 'visitorsByLocation' ] > {
+ return {
+ queryKey: getReportVisitorsByLocationQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportVisitorsByLocation( params );
+ return sanitizeReportVisitorsByLocationResponse( response );
+ },
+
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts
new file mode 100644
index 000000000000..78d2f138d554
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts
@@ -0,0 +1,43 @@
+/**
+ * External dependencies
+ */
+
+/**
+ * Internal dependencies
+ */
+import { fetchReportVisitors } from '../api';
+import { sanitizeReportVisitorsResponse } from '../processing/visitors';
+import type { ReportDataMap } from '../types';
+import type { UseQueryOptions } from '@tanstack/react-query';
+
+type RequestReportVisitorsParams = Parameters< typeof fetchReportVisitors >[ 0 ];
+
+const getReportVisitorsQueryKey = ( p: RequestReportVisitorsParams ) =>
+ [ 'reports', 'visitors', 'by-date', p.from, p.to, p.interval, p.date_type ] as const;
+
+/**
+ *
+ * @param params
+ */
+export function reportVisitorsQuery(
+ params: RequestReportVisitorsParams
+): UseQueryOptions< ReportDataMap[ 'visitors' ] > {
+ return {
+ queryKey: getReportVisitorsQueryKey( params ),
+ queryFn: async () => {
+ const response = await fetchReportVisitors( params );
+ return sanitizeReportVisitorsResponse( response );
+ },
+
+ /**
+ * Enable the query only if the from, to, and interval are set.
+ */
+ enabled: !! ( params.from && params.to && params.interval ),
+
+ /**
+ * Keep previous data while fetching new data to prevent blank states
+ * @param previousData
+ */
+ placeholderData: previousData => previousData,
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/types.ts b/projects/packages/premium-analytics/packages/data/src/types.ts
new file mode 100644
index 000000000000..76b5d9f1a945
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/types.ts
@@ -0,0 +1,99 @@
+/**
+ * Internal dependencies
+ */
+import { sanitizeReportOrdersResponse, sanitizeReportProductsResponse } from './processing';
+import { sanitizeReportBookingsResponse } from './processing/bookings';
+import { sanitizeReportConversionRateResponse } from './processing/conversion-rate';
+import { sanitizeReportCouponsResponse } from './processing/coupons';
+import { sanitizeReportCouponsByDateResponse } from './processing/coupons-by-date';
+import { sanitizeReportCustomersResponse } from './processing/customers';
+import { sanitizeReportCustomersByDateResponse } from './processing/customers-by-date';
+import { sanitizeReportOrderAttributionSummaryResponse } from './processing/order-attribution';
+import { sanitizeReportOrdersByProductTypeResponse } from './processing/orders-by-product-type';
+import { sanitizeReportSessionsByDeviceResponse } from './processing/sessions-by-device';
+import { sanitizeReportVisitorsResponse } from './processing/visitors';
+import { sanitizeReportVisitorsByLocationResponse } from './processing/visitors-by-location';
+import type { ReportParams } from './utils/search';
+
+export type ReportType =
+ | 'orders'
+ | 'orders-by-product-type'
+ | 'order-attribution'
+ | 'coupons'
+ | 'couponsByDate'
+ | 'customers'
+ | 'customersByDate'
+ | 'products'
+ | 'visitors'
+ | 'visitorsByLocation'
+ | 'conversionRate'
+ | 'bookings'
+ | 'sessionsByDevice';
+
+export type QueryParams = ReportParams & {
+ p?: string; // encoded pathname
+};
+
+// Inferred from processing/orders.ts
+type SanitizedOrdersByDateResponse = ReturnType< typeof sanitizeReportOrdersResponse >;
+
+// Inferred from processing/order-attribution.ts
+type SanitizedOrderAttributionSummaryResponse = ReturnType<
+ typeof sanitizeReportOrderAttributionSummaryResponse
+>;
+
+// Inferred from processing/coupons.ts
+type SanitizedCouponsResponse = ReturnType< typeof sanitizeReportCouponsResponse >;
+
+// Inferred from processing/coupons-by-date/index.ts
+type SanitizedCouponsByDateResponse = ReturnType< typeof sanitizeReportCouponsByDateResponse >;
+
+// Inferred from processing/customers.ts
+type SanitizedCustomersResponse = ReturnType< typeof sanitizeReportCustomersResponse >;
+
+// Inferred from processing/customers-by-date/index.ts
+type SanitizedCustomersByDateResponse = ReturnType< typeof sanitizeReportCustomersByDateResponse >;
+
+// Inferred from processing/products.ts
+type SanitizedProductsResponse = ReturnType< typeof sanitizeReportProductsResponse >;
+
+// Inferred from processing/visitors.ts
+type SanitizedVisitorsResponse = ReturnType< typeof sanitizeReportVisitorsResponse >;
+
+// Inferred from processing/visitors-by-location.ts
+type SanitizedVisitorsByLocationResponse = ReturnType<
+ typeof sanitizeReportVisitorsByLocationResponse
+>;
+
+// Inferred from processing/conversion-rate.ts
+type SanitizedConversionRateResponse = ReturnType< typeof sanitizeReportConversionRateResponse >;
+
+// Inferred from processing/orders-by-product-type.ts
+type SanitizedOrdersByProductTypeResponse = ReturnType<
+ typeof sanitizeReportOrdersByProductTypeResponse
+>;
+
+// Inferred from processing/bookings.ts
+type SanitizedBookingsResponse = ReturnType< typeof sanitizeReportBookingsResponse >;
+
+// Inferred from processing/sessions-by-device.ts
+type SanitizedSessionsByDeviceResponse = ReturnType<
+ typeof sanitizeReportSessionsByDeviceResponse
+>;
+
+// Type mapping for report types to their PROCESSED data structures
+export interface ReportDataMap {
+ orders: SanitizedOrdersByDateResponse; // Returns processed data with numbers
+ 'orders-by-product-type': SanitizedOrdersByProductTypeResponse; // Returns processed orders by product type data with numbers
+ 'order-attribution': SanitizedOrderAttributionSummaryResponse; // Returns processed attribution data
+ coupons: SanitizedCouponsResponse; // Returns processed coupons data with numbers
+ couponsByDate: SanitizedCouponsByDateResponse; // Returns processed coupons-by-date data with numbers
+ customers: SanitizedCustomersResponse; // Returns processed customers data with numbers
+ customersByDate: SanitizedCustomersByDateResponse; // Returns processed customers by date data with numbers
+ products: SanitizedProductsResponse; // Returns raw products data
+ visitors: SanitizedVisitorsResponse; // Returns processed visitors data with numbers
+ visitorsByLocation: SanitizedVisitorsByLocationResponse; // Returns processed visitors grouped by location (country or region)
+ conversionRate: SanitizedConversionRateResponse; // Returns processed conversion rate data with numbers
+ bookings: SanitizedBookingsResponse; // Returns processed bookings data with numbers
+ sessionsByDevice: SanitizedSessionsByDeviceResponse; // Returns processed sessions by device data with numbers
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts b/projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts
new file mode 100644
index 000000000000..d6bb433880a2
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts
@@ -0,0 +1,23 @@
+/**
+ * Filter condition types for API queries
+ */
+
+// Different type of filters have different comparison operators
+// @see https://github.a8c.com/Automattic/wpcom/tree/72572945acd96d29adf9ea8f38fc3e99c9a4a668/wp-content/rest-api-plugins/endpoints/woocommerce-analytics/Reports/Filter
+export type FilterCondition = {
+ key: string;
+ value: string | string[];
+ compare:
+ | '='
+ | 'IN'
+ | 'NOT IN'
+ | '!='
+ | '>'
+ | '<'
+ | '>='
+ | '<='
+ | 'BETWEEN'
+ | 'NOT BETWEEN'
+ | 'LIKE'
+ | 'NOT LIKE';
+};
diff --git a/projects/packages/premium-analytics/packages/data/src/types/product-image.ts b/projects/packages/premium-analytics/packages/data/src/types/product-image.ts
new file mode 100644
index 000000000000..8fc5b65ad822
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/types/product-image.ts
@@ -0,0 +1,7 @@
+/**
+ * Product image type definition
+ */
+export interface ProductImage {
+ imageUrl: string;
+ imageAlt: string;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/types/product-type.ts b/projects/packages/premium-analytics/packages/data/src/types/product-type.ts
new file mode 100644
index 000000000000..2b5a41ab1cf8
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/types/product-type.ts
@@ -0,0 +1,4 @@
+/**
+ * Product type categories for filtering and organization
+ */
+export type ProductType = 'general' | 'products' | 'bookings';
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts
new file mode 100644
index 000000000000..e6c2ea4e4a58
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts
@@ -0,0 +1,150 @@
+/**
+ * External dependencies
+ */
+import { tz } from '@date-fns/tz';
+import {
+ startOfDay,
+ endOfDay,
+ subDays,
+ subMonths,
+ subYears,
+ startOfMonth,
+ endOfMonth,
+ startOfYear,
+ endOfYear,
+} from 'date-fns';
+/**
+ * Mocks – getSiteTimezone and dateToISOStringWithLocalTZ
+ * depend on WordPress core store.
+ * We mock them to remove that dependency.
+ *
+ * dateToISOStringWithLocalTZ normalizes to UTC Z-format
+ * (matching native Date.toISOString) since the mock timezone
+ * is +00:00 and all dates are UTC.
+ */
+jest.mock( '../date', () => ( {
+ getSiteTimezone: jest.fn( () => '+00:00' ),
+ dateToISOStringWithLocalTZ: jest.fn( ( date: Date ) => new Date( date.getTime() ).toISOString() ),
+} ) );
+/**
+ * Internal dependencies
+ */
+import { computeDateRangeFromPreset } from '../preset-date-range';
+
+/*
+ * Pin "now" to 2026-02-19 12:00:00 UTC for deterministic results.
+ *
+ * Expected dates are computed in UTC. Since TZ is mocked to +00:00,
+ * computePrimaryRange runs in UTC and dateToISOStringWithLocalTZ
+ * normalizes to Z-format via the mock.
+ */
+const NOW = new Date( '2026-02-19T12:00:00.000Z' );
+const UTC = tz( '+00:00' );
+
+/*
+ * Normalize a TZDate or Date to Z-format ISO string,
+ * ensuring the expected values match the mock's output format.
+ */
+/**
+ *
+ * @param date
+ */
+function toZ( date: Date ): string {
+ return new Date( date.getTime() ).toISOString();
+}
+
+const TODAY_START = startOfDay( NOW, { in: UTC } );
+const TODAY_END = endOfDay( NOW, { in: UTC } );
+const YESTERDAY_END = endOfDay( subDays( TODAY_START, 1 ), { in: UTC } );
+const LAST_MONTH = subMonths( TODAY_START, 1 );
+
+beforeAll( () => {
+ jest.useFakeTimers();
+ jest.setSystemTime( NOW );
+} );
+
+afterAll( () => {
+ jest.useRealTimers();
+} );
+
+describe( 'computeDateRangeFromPreset', () => {
+ it( 'returns today range for "today"', () => {
+ const range = computeDateRangeFromPreset( 'today' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( TODAY_START ) );
+ expect( range!.to ).toBe( toZ( TODAY_END ) );
+ } );
+
+ it( 'returns yesterday range for "yesterday"', () => {
+ const range = computeDateRangeFromPreset( 'yesterday' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( subDays( TODAY_START, 1 ) ) );
+ expect( range!.to ).toBe( toZ( YESTERDAY_END ) );
+ } );
+
+ it( 'returns 7-day range ending yesterday for "last-7-days"', () => {
+ const range = computeDateRangeFromPreset( 'last-7-days' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( subDays( TODAY_START, 7 ) ) );
+ expect( range!.to ).toBe( toZ( YESTERDAY_END ) );
+ } );
+
+ it( 'returns 30-day range ending yesterday for "last-30-days"', () => {
+ const range = computeDateRangeFromPreset( 'last-30-days' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( subDays( TODAY_START, 30 ) ) );
+ expect( range!.to ).toBe( toZ( YESTERDAY_END ) );
+ } );
+
+ it( 'returns 90-day range ending yesterday for "last-90-days"', () => {
+ const range = computeDateRangeFromPreset( 'last-90-days' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( subDays( TODAY_START, 90 ) ) );
+ expect( range!.to ).toBe( toZ( YESTERDAY_END ) );
+ } );
+
+ it( 'returns 365-day range ending yesterday for "last-365-days"', () => {
+ const range = computeDateRangeFromPreset( 'last-365-days' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( subDays( TODAY_START, 365 ) ) );
+ expect( range!.to ).toBe( toZ( YESTERDAY_END ) );
+ } );
+
+ it( 'returns last calendar month for "last-month"', () => {
+ const range = computeDateRangeFromPreset( 'last-month' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( startOfMonth( LAST_MONTH, { in: UTC } ) ) );
+ expect( range!.to ).toBe( toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) );
+ } );
+
+ it( 'returns last 12 calendar months for "last-12-months"', () => {
+ const range = computeDateRangeFromPreset( 'last-12-months' );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( startOfMonth( subMonths( TODAY_START, 12 ), { in: UTC } ) ) );
+ expect( range!.to ).toBe( toZ( endOfMonth( LAST_MONTH, { in: UTC } ) ) );
+ } );
+
+ it( 'returns last calendar year for "last-year"', () => {
+ const range = computeDateRangeFromPreset( 'last-year' );
+ const lastYear = subYears( TODAY_START, 1 );
+
+ expect( range ).toBeDefined();
+ expect( range!.from ).toBe( toZ( startOfYear( lastYear, { in: UTC } ) ) );
+ expect( range!.to ).toBe( toZ( endOfYear( lastYear, { in: UTC } ) ) );
+ } );
+
+ it( 'returns undefined for unrecognized preset', () => {
+ // @ts-expect-error – testing with invalid preset on purpose
+ const range = computeDateRangeFromPreset( 'not-a-preset' );
+
+ expect( range ).toBeUndefined();
+ } );
+} );
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts
new file mode 100644
index 000000000000..1dc771865b48
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts
@@ -0,0 +1,81 @@
+/**
+ * Mocks – break the dependency chain to `@wordpress/core-data`.
+ */
+jest.mock( '../../defaults', () => ( {
+ getDefaultQueryParams: jest.fn(),
+} ) );
+
+jest.mock( '../preset-date-range', () => ( {
+ computeDateRangeFromPreset: jest.fn(),
+} ) );
+
+jest.mock( '../interval', () => ( {
+ getDefaultIntervalForPeriod: jest.fn(),
+} ) );
+/**
+ * Internal dependencies
+ */
+import { hasComparisonEnabled } from '../search';
+
+describe( 'hasComparisonEnabled', () => {
+ it( 'returns true when all comparison fields are present', () => {
+ expect(
+ hasComparisonEnabled( {
+ comp: '1',
+ compare_from: '2026-01-01T00:00:00.000-05:00',
+ compare_to: '2026-01-31T23:59:59.999-05:00',
+ } )
+ ).toBe( true );
+ } );
+
+ it( 'returns false when comp is undefined', () => {
+ expect(
+ hasComparisonEnabled( {
+ compare_from: '2026-01-01T00:00:00.000-05:00',
+ compare_to: '2026-01-31T23:59:59.999-05:00',
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'returns false when compare_from is missing', () => {
+ expect(
+ hasComparisonEnabled( {
+ comp: '1',
+ compare_to: '2026-01-31T23:59:59.999-05:00',
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'returns false when compare_to is missing', () => {
+ expect(
+ hasComparisonEnabled( {
+ comp: '1',
+ compare_from: '2026-01-01T00:00:00.000-05:00',
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'returns false when compare_from is whitespace', () => {
+ expect(
+ hasComparisonEnabled( {
+ comp: '1',
+ compare_from: ' ',
+ compare_to: '2026-01-31T23:59:59.999-05:00',
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'returns false when compare_to is whitespace', () => {
+ expect(
+ hasComparisonEnabled( {
+ comp: '1',
+ compare_from: '2026-01-01T00:00:00.000-05:00',
+ compare_to: ' ',
+ } )
+ ).toBe( false );
+ } );
+
+ it( 'returns false for empty object', () => {
+ expect( hasComparisonEnabled( {} ) ).toBe( false );
+ } );
+} );
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts
new file mode 100644
index 000000000000..00644be04800
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts
@@ -0,0 +1,291 @@
+/**
+ * Mocks – must appear before the import of the module under test.
+ */
+jest.mock( '../../defaults', () => ( {
+ getDefaultQueryParams: jest.fn(),
+} ) );
+
+jest.mock( '../preset-date-range', () => ( {
+ computeDateRangeFromPreset: jest.fn(),
+} ) );
+
+jest.mock( '../interval', () => ( {
+ getDefaultIntervalForPeriod: jest.fn(),
+} ) );
+/**
+ * Internal dependencies
+ */
+import { getDefaultQueryParams } from '../../defaults';
+import { getDefaultIntervalForPeriod } from '../interval';
+import { computeDateRangeFromPreset } from '../preset-date-range';
+import { normalizeReportParams } from '../search';
+import type { ReportParams } from '../search';
+
+const mockGetDefaults = getDefaultQueryParams as jest.MockedFunction<
+ typeof getDefaultQueryParams
+>;
+const mockComputeRange = computeDateRangeFromPreset as jest.MockedFunction<
+ typeof computeDateRangeFromPreset
+>;
+const mockGetInterval = getDefaultIntervalForPeriod as jest.MockedFunction<
+ typeof getDefaultIntervalForPeriod
+>;
+
+/*
+ * Deterministic date strings.
+ * FRESH = what computeDateRangeFromPreset returns "today".
+ * STALE = what the URL had from a previous day.
+ */
+const FRESH_FROM = '2026-01-20T00:00:00.000-05:00';
+const FRESH_TO = '2026-02-18T23:59:59.999-05:00';
+const STALE_FROM = '2026-01-19T00:00:00.000-05:00';
+const STALE_TO = '2026-02-17T23:59:59.999-05:00';
+
+const DEFAULTS_WITH_COMPARISON: ReportParams = {
+ from: FRESH_FROM,
+ to: FRESH_TO,
+ preset: 'last-30-days',
+ interval: 'day',
+ compare_from: '2025-12-21T00:00:00.000-05:00',
+ compare_to: '2026-01-19T23:59:59.999-05:00',
+ compare_preset: 'previous-period',
+ comp: '1',
+};
+
+beforeEach( () => {
+ jest.clearAllMocks();
+
+ // Sensible defaults for every test – override per-scenario as needed.
+ mockGetDefaults.mockReturnValue( { ...DEFAULTS_WITH_COMPARISON } );
+ mockComputeRange.mockReturnValue( {
+ from: FRESH_FROM,
+ to: FRESH_TO,
+ } );
+ mockGetInterval.mockReturnValue( 'day' );
+} );
+
+describe( 'normalizeReportParams', () => {
+ /*
+ * Scenario 1 – Fresh load (no params in URL)
+ * The user visits /dashboard with no query string.
+ * Expected: defaults kick in, preset "last-30-days" is used,
+ * and default comparison is applied.
+ */
+ it( 'applies defaults with preset and comparison on fresh load', () => {
+ const result = normalizeReportParams();
+
+ // Preset should come from defaults.
+ expect( result.preset ).toBe( 'last-30-days' );
+ expect( mockComputeRange ).toHaveBeenCalledWith( 'last-30-days' );
+
+ // Dates should come from computeDateRangeFromPreset.
+ expect( result.from ).toBe( FRESH_FROM );
+ expect( result.to ).toBe( FRESH_TO );
+
+ // Default comparison should be applied (search is undefined
+ // → !search?.from → true → default branch).
+ expect( result.comp ).toBe( '1' );
+ expect( result.compare_from ).toBe( DEFAULTS_WITH_COMPARISON.compare_from );
+ expect( result.compare_to ).toBe( DEFAULTS_WITH_COMPARISON.compare_to );
+ } );
+
+ /*
+ * Scenario 2 – Same-day reload with preset
+ * The URL has preset=last-30-days and from/to that match today's
+ * computation. The dates are still fresh → no redirect needed.
+ */
+ it( 'returns same dates when preset range is still fresh', () => {
+ const result = normalizeReportParams( {
+ from: FRESH_FROM,
+ to: FRESH_TO,
+ preset: 'last-30-days',
+ interval: 'day',
+ } );
+
+ expect( result.from ).toBe( FRESH_FROM );
+ expect( result.to ).toBe( FRESH_TO );
+ expect( result.preset ).toBe( 'last-30-days' );
+
+ // No comparison in search → no comparison in output
+ // (search.from is present → !search.from is false
+ // → default comparison branch is skipped).
+ expect( result.comp ).toBeUndefined();
+ } );
+
+ /*
+ * Scenario 3 – Next-day reload with stale dates
+ * The URL has yesterday's dates but the same preset.
+ * computeDateRangeFromPreset returns fresh dates → redirect.
+ */
+ it( 'recalculates dates when preset range is stale', () => {
+ const result = normalizeReportParams( {
+ from: STALE_FROM,
+ to: STALE_TO,
+ preset: 'last-30-days',
+ interval: 'day',
+ } );
+
+ // Should use the fresh range from the preset, not stale URL dates.
+ expect( result.from ).toBe( FRESH_FROM );
+ expect( result.to ).toBe( FRESH_TO );
+ expect( result.preset ).toBe( 'last-30-days' );
+ expect( mockComputeRange ).toHaveBeenCalledWith( 'last-30-days' );
+ } );
+
+ /*
+ * Scenario 4 – Custom range (no preset)
+ * The user picked explicit from/to dates without a preset.
+ * The dates should be used as-is, no recalculation.
+ */
+ it( 'uses explicit dates as-is when no preset is set', () => {
+ const customFrom = '2026-01-01T00:00:00.000-05:00';
+ const customTo = '2026-01-31T23:59:59.999-05:00';
+
+ const result = normalizeReportParams( {
+ from: customFrom,
+ to: customTo,
+ } );
+
+ expect( result.from ).toBe( customFrom );
+ expect( result.to ).toBe( customTo );
+ expect( result.preset ).toBeUndefined();
+ // computeDateRangeFromPreset should NOT be called.
+ expect( mockComputeRange ).not.toHaveBeenCalled();
+ } );
+
+ it( 'uses explicit dates as-is when preset is custom', () => {
+ const customFrom = '2026-01-01T00:00:00.000-05:00';
+ const customTo = '2026-01-31T23:59:59.999-05:00';
+
+ const result = normalizeReportParams( {
+ from: customFrom,
+ to: customTo,
+ preset: 'custom',
+ } );
+
+ expect( result.from ).toBe( customFrom );
+ expect( result.to ).toBe( customTo );
+ expect( result.preset ).toBeUndefined();
+ expect( mockComputeRange ).not.toHaveBeenCalled();
+ } );
+
+ /*
+ * Scenario 5 – Preset with stale comparison enabled
+ * The URL has a stale preset and comparison params.
+ * Primary range is recalculated; comparison is preserved from URL.
+ */
+ it( 'recalculates primary but preserves comparison from URL', () => {
+ const compFrom = '2025-12-20T00:00:00.000-05:00';
+ const compTo = '2026-01-18T23:59:59.999-05:00';
+
+ const result = normalizeReportParams( {
+ from: STALE_FROM,
+ to: STALE_TO,
+ preset: 'last-30-days',
+ interval: 'day',
+ comp: '1',
+ compare_from: compFrom,
+ compare_to: compTo,
+ compare_preset: 'previous-period',
+ } );
+
+ // Primary recalculated from preset.
+ expect( result.from ).toBe( FRESH_FROM );
+ expect( result.to ).toBe( FRESH_TO );
+
+ // Comparison passed through from search.
+ expect( result.comp ).toBe( '1' );
+ expect( result.compare_from ).toBe( compFrom );
+ expect( result.compare_to ).toBe( compTo );
+ expect( result.compare_preset ).toBe( 'previous-period' );
+ } );
+
+ /*
+ * Scenario 6 – Preset without comparison
+ * The URL has a stale preset but comparison is disabled.
+ * Primary is recalculated; comparison params are absent.
+ */
+ it( 'recalculates primary with no comparison when comp is absent', () => {
+ const result = normalizeReportParams( {
+ from: STALE_FROM,
+ to: STALE_TO,
+ preset: 'last-30-days',
+ interval: 'day',
+ } );
+
+ // Primary recalculated.
+ expect( result.from ).toBe( FRESH_FROM );
+ expect( result.to ).toBe( FRESH_TO );
+
+ // No comparison in search, and search.from is present
+ // → default comparison is NOT applied.
+ expect( result.comp ).toBeUndefined();
+ expect( result.compare_from ).toBeUndefined();
+ expect( result.compare_to ).toBeUndefined();
+ } );
+
+ /*
+ * Edge case – Invalid preset in URL is ignored.
+ */
+ it( 'ignores invalid preset and uses URL dates', () => {
+ const customFrom = '2026-02-01T00:00:00.000-05:00';
+ const customTo = '2026-02-15T23:59:59.999-05:00';
+
+ const result = normalizeReportParams( {
+ from: customFrom,
+ to: customTo,
+ // @ts-expect-error – testing with invalid preset on purpose
+ preset: 'not-a-real-preset',
+ } );
+
+ expect( result.from ).toBe( customFrom );
+ expect( result.to ).toBe( customTo );
+ expect( result.preset ).toBeUndefined();
+ expect( mockComputeRange ).not.toHaveBeenCalled();
+ } );
+
+ /*
+ * Edge case – computeDateRangeFromPreset returns undefined
+ * (e.g., an unimplemented preset). Falls back to search dates.
+ */
+ it( 'falls back to URL dates when preset has no range implementation', () => {
+ mockComputeRange.mockReturnValue( undefined );
+
+ const result = normalizeReportParams( {
+ from: STALE_FROM,
+ to: STALE_TO,
+ preset: 'last-30-days',
+ } );
+
+ // Preset should be cleared.
+ expect( result.preset ).toBeUndefined();
+ // Falls back to search dates.
+ expect( result.from ).toBe( STALE_FROM );
+ expect( result.to ).toBe( STALE_TO );
+ } );
+
+ /*
+ * Edge case – date_type is preserved from search.
+ */
+ it( 'preserves date_type from search', () => {
+ const result = normalizeReportParams( {
+ from: FRESH_FROM,
+ to: FRESH_TO,
+ date_type: 'paid',
+ } );
+
+ expect( result.date_type ).toBe( 'paid' );
+ } );
+
+ /*
+ * Edge case – date_type defaults to "created".
+ */
+ it( 'defaults date_type to created', () => {
+ const result = normalizeReportParams( {
+ from: FRESH_FROM,
+ to: FRESH_TO,
+ } );
+
+ expect( result.date_type ).toBe( 'created' );
+ } );
+} );
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/date.ts b/projects/packages/premium-analytics/packages/data/src/utils/date.ts
new file mode 100644
index 000000000000..81f1950d8d4e
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts
@@ -0,0 +1,114 @@
+/**
+ * External dependencies
+ */
+import { type TZDate } from '@date-fns/tz';
+import {
+ toLocalTZ,
+ formatToTimezoneNaiveString as _formatNaive,
+ dateToISOStringWithTZ as _toISOWithTZ,
+} from '@jetpack-premium-analytics/datetime';
+import { store as coreStore, type Settings } from '@wordpress/core-data';
+import { select } from '@wordpress/data';
+
+type FullSettings = Settings & {
+ gmt_offset: number;
+};
+
+let DEFAULT_TIME_ZONE: string;
+try {
+ DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone ?? '+00:00';
+} catch {
+ DEFAULT_TIME_ZONE = '+00:00';
+}
+
+/**
+ * Format the GMT offset to a string.
+ *
+ * @param {number | undefined} offset - The GMT offset.
+ * @return {string} The formatted GMT offset.
+ */
+function formatGmtOffset( offset: number | undefined ): string {
+ if ( ! offset ) {
+ return DEFAULT_TIME_ZONE;
+ }
+
+ const sign = offset >= 0 ? '+' : '-';
+ const abs = Math.abs( offset );
+ const hours = Math.floor( abs );
+ const minutes = Math.floor( ( abs - hours ) * 60 + 1e-6 );
+ return `${ sign }${ String( hours ).padStart( 2, '0' ) }:${ String( minutes ).padStart(
+ 2,
+ '0'
+ ) }`;
+}
+
+/*
+ * Get the timezone from the site settings.
+ * If the timezone is not set, use the GMT offset.
+ * If the GMT offset is not set, use the default timezone.
+ *
+ * @param {string} timezone - The timezone to use.
+ * @return {string} The timezone.
+ */
+/**
+ *
+ */
+export function getSiteTimezone() {
+ const siteSettings = select( coreStore ).getEntityRecord( 'root', 'site' ) as FullSettings;
+
+ if ( ! siteSettings ) {
+ return DEFAULT_TIME_ZONE;
+ }
+
+ return siteSettings?.timezone?.length
+ ? siteSettings?.timezone
+ : formatGmtOffset( siteSettings?.gmt_offset ) || DEFAULT_TIME_ZONE;
+}
+
+/**
+ * Returns the site's GMT offset as a string (e.g. "+05:30", "-08:00").
+ * If site settings are not loaded, throws an error.
+ * @return {string} The site's GMT offset.
+ */
+export function getSiteGmtOffset(): string {
+ const siteSettings = select( coreStore ).getEntityRecord( 'root', 'site' ) as FullSettings;
+ if ( ! siteSettings ) {
+ throw new Error( 'getSiteGmtOffset() called before core settings are ready' );
+ }
+ return formatGmtOffset( siteSettings?.gmt_offset ) || DEFAULT_TIME_ZONE;
+}
+
+/**
+ * Same API and behavior as your current localTZDate:
+ * - Accepts number | string | Date (or undefined -> now)
+ * - Uses site timezone by default
+ * - Returns TZDate (timezone-aware)
+ * @param value
+ * @param timezone
+ */
+export function localTZDate( value?: number | string | Date, timezone?: string ): TZDate {
+ const tz = timezone ?? getSiteTimezone();
+ return toLocalTZ( value, tz );
+}
+
+/**
+ * Same semantics as your current helper:
+ * TZ-aware -> timezone-naive "YYYY-MM-DDTHH:mm:ss.SSS"
+ * @param date
+ * @param timezone
+ */
+export function formatToTimezoneNaiveString( date: Date, timezone?: string ): string {
+ const tz = timezone ?? getSiteTimezone();
+ return _formatNaive( date, tz );
+}
+
+/**
+ * Same semantics as your current helper:
+ * TZ-aware -> ISO with offset "YYYY-MM-DDTHH:mm:ss.SSSxxx"
+ * @param date
+ * @param timezone
+ */
+export function dateToISOStringWithLocalTZ( date: Date, timezone?: string ): string {
+ const tz = timezone ?? getSiteTimezone();
+ return _toISOWithTZ( date, tz );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts
new file mode 100644
index 000000000000..cd00c8a6e0e5
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts
@@ -0,0 +1,21 @@
+/**
+ * External dependencies
+ */
+import { store as coreStore } from '@wordpress/core-data';
+import { resolveSelect } from '@wordpress/data';
+
+let readyPromise: Promise< void > | null = null;
+
+/**
+ * Ensures that 'site' and 'general settings' are in the coreStore.
+ * Memoizes the same promise to avoid races and duplicate requests.
+ */
+export function ensureCoreSettingsReady(): Promise< void > {
+ if ( ! readyPromise ) {
+ readyPromise = Promise.all( [
+ resolveSelect( coreStore ).getEntityRecord( 'root', 'site' ),
+ resolveSelect( coreStore ).getEntityRecord( 'root', 'settings', 'general' ),
+ ] ).then( () => void 0 );
+ }
+ return readyPromise;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/index.ts b/projects/packages/premium-analytics/packages/data/src/utils/index.ts
new file mode 100644
index 000000000000..0a0763050244
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/index.ts
@@ -0,0 +1,15 @@
+export {
+ localTZDate,
+ dateToISOStringWithLocalTZ,
+ formatToTimezoneNaiveString,
+ getSiteTimezone,
+ getSiteGmtOffset,
+} from './date';
+export { ensureCoreSettingsReady } from './ensure-core-settings';
+export { getDefaultIntervalForPeriod } from './interval';
+export { safeParseInt, safeParseFloat } from './parsing';
+export { computeDateRangeFromPreset } from './preset-date-range';
+export { hasProductFilters } from './product-filters';
+export type { PresetType, ReportParams } from './search';
+export { isSelectablePreset } from '@jetpack-premium-analytics/datetime';
+export type { Override, BaseReportParams } from './types';
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts
new file mode 100644
index 000000000000..30c962cb435d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts
@@ -0,0 +1,116 @@
+/**
+ * External dependencies
+ */
+import { differenceInHours } from 'date-fns';
+/**
+ * Internal dependencies
+ */
+import { localTZDate } from './date';
+import type { IntervalType } from './search';
+
+/**
+ *
+ * @param from
+ * @param to
+ */
+function getAllowedIntervalsByRange( from: string, to: string ): IntervalType[] {
+ // Use hours instead of days to handle ranges that are 1 second short of a full day.
+ // E.g., '2024-11-01 00:00:00' to '2025-10-31 23:59:59' is 8759 hours (364.958 days),
+ // which rounds to 365 days, correctly categorizing it as a yearly interval.
+ const daysDiff = Math.round(
+ Math.abs( differenceInHours( localTZDate( to ), localTZDate( from ) ) / 24 )
+ );
+
+ if ( daysDiff >= 1095 ) {
+ return [ 'quarter', 'year' ];
+ } else if ( daysDiff >= 365 ) {
+ return [ 'month', 'quarter' ];
+ } else if ( daysDiff >= 90 ) {
+ return [ 'week', 'month' ];
+ } else if ( daysDiff >= 28 ) {
+ return [ 'day', 'week' ];
+ } else if ( daysDiff >= 3 ) {
+ return [ 'day' ];
+ } else if ( daysDiff >= 1 ) {
+ return [ 'hour', 'day' ];
+ }
+
+ return [ 'hour', 'day' ];
+}
+
+/**
+ * Returns the allowed selectable intervals for a specific period.
+ *
+ * @param period
+ * @param from
+ * @param to
+ * @return {Array} Array containing allowed intervals.
+ */
+function getAllowedIntervalsForPeriod(
+ period: string | undefined,
+ from: string,
+ to: string
+): IntervalType[] {
+ switch ( period ) {
+ case 'today':
+ case 'yesterday':
+ return [ 'hour', 'day' ];
+ case 'last-7-days':
+ return [ 'day' ];
+ case 'last-30-days':
+ case 'last-month':
+ return [ 'day', 'week' ];
+ case 'last-90-days':
+ return [ 'week', 'month' ];
+ case 'last-12-months':
+ case 'last-365-days':
+ case 'last-year':
+ return [ 'month', 'quarter' ];
+ default:
+ return getAllowedIntervalsByRange( from, to );
+ }
+}
+
+/**
+ *
+ * @param period
+ * @param from
+ * @param to
+ */
+export function getDefaultIntervalForPeriod(
+ period: string | undefined,
+ from: string,
+ to: string
+): IntervalType {
+ return getAllowedIntervalsForPeriod( period, from, to )?.[ 0 ] ?? 'day';
+}
+
+/**
+ *
+ * @param period
+ * @param from
+ * @param to
+ */
+export function getDateFormatFromInterval(
+ period: string | undefined, // Pass in undefined to use the default interval.
+ from: string,
+ to: string
+): string {
+ const interval = getDefaultIntervalForPeriod( period, from, to );
+
+ switch ( interval ) {
+ case 'hour':
+ return 'HH:mm';
+ case 'day':
+ case 'week':
+ return 'MMM d';
+ case 'month':
+ return 'MMM yyyy';
+ case 'quarter':
+ return 'qqq yyyy';
+ case 'year':
+ return 'yyyy';
+ default:
+ return 'MMM d';
+ }
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts
new file mode 100644
index 000000000000..8a4535a7231d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts
@@ -0,0 +1,19 @@
+/**
+ * Safe integer parsing with fallback value
+ * @param value
+ * @param fallback
+ */
+export function safeParseInt( value: unknown, fallback = 0 ): number {
+ const num = parseInt( String( value ), 10 );
+ return isNaN( num ) ? fallback : num;
+}
+
+/**
+ * Safe float parsing with fallback value
+ * @param value
+ * @param fallback
+ */
+export function safeParseFloat( value: unknown, fallback = 0 ): number {
+ const num = parseFloat( String( value ) );
+ return isNaN( num ) ? fallback : num;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts
new file mode 100644
index 000000000000..cf0345d1003d
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts
@@ -0,0 +1,34 @@
+/**
+ * External dependencies
+ */
+import { computePrimaryRange } from '@jetpack-premium-analytics/datetime';
+import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date';
+import type { SelectablePresetId } from '@jetpack-premium-analytics/datetime';
+/**
+ * Internal dependencies
+ */
+
+/**
+ * Compute the absolute date range for a given preset ID
+ * based on the current date and the site's timezone.
+ *
+ * Thin wrapper over datetime's computePrimaryRange that
+ * resolves the site timezone and converts Date -> ISO string.
+ *
+ * @param presetId - A valid selectable preset identifier.
+ * @return The computed { from, to } ISO strings, or undefined
+ * if the preset is not recognized.
+ */
+export function computeDateRangeFromPreset(
+ presetId: SelectablePresetId
+): { from: string; to: string } | undefined {
+ const range = computePrimaryRange( presetId, getSiteTimezone() );
+ if ( ! range?.from || ! range?.to ) {
+ return undefined;
+ }
+
+ return {
+ from: dateToISOStringWithLocalTZ( range.from ),
+ to: dateToISOStringWithLocalTZ( range.to ),
+ };
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts
new file mode 100644
index 000000000000..063b924a50b2
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts
@@ -0,0 +1,23 @@
+/**
+ * Internal dependencies
+ */
+import type { FilterCondition } from '../types/filter-condition';
+
+/**
+ * Product lookup table level filters.
+ */
+const PRODUCT_FILTER_KEYS = [ 'product_type', 'virtual', 'downloadable' ];
+
+/**
+ * Checks if any of the provided filters are product-related filters
+ *
+ * @param filters - Array of filter conditions to check
+ * @return True if any filter is product-related, false otherwise
+ */
+export function hasProductFilters( filters?: FilterCondition[] ): boolean {
+ if ( ! filters || ! Array.isArray( filters ) || filters.length === 0 ) {
+ return false;
+ }
+
+ return filters.some( filter => PRODUCT_FILTER_KEYS.includes( filter.key ) );
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/search.ts b/projects/packages/premium-analytics/packages/data/src/utils/search.ts
new file mode 100644
index 000000000000..0aa97aacb501
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts
@@ -0,0 +1,145 @@
+/**
+ * External dependencies
+ */
+import {
+ isSelectablePreset,
+ type SelectablePresetId,
+ type ComparisonPresetId,
+ type PrimaryPresetId,
+} from '@jetpack-premium-analytics/datetime';
+/**
+ * Internal dependencies
+ */
+import { ORDER_ATTRIBUTION_VIEWS } from '../api/report-order-attribution-summary-fetch';
+import { getDefaultQueryParams } from '../defaults';
+import { getDefaultIntervalForPeriod } from './interval';
+import { computeDateRangeFromPreset } from './preset-date-range';
+import type { DateType } from './types';
+import type { FilterCondition } from '../types/filter-condition';
+
+export type { FilterCondition };
+
+/**
+ * Re-export SelectablePresetId as PresetType for backward compatibility.
+ * The canonical type now lives in `@jetpack-premium-analytics/datetime`.
+ */
+export type PresetType = SelectablePresetId;
+
+type OrderAttributionView = ( typeof ORDER_ATTRIBUTION_VIEWS )[ number ];
+
+export type IntervalType = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year';
+
+/*
+ * ReportParams are the expected params present in the client URL.
+ * They aren't meant to be the reports params
+ * of the API endpoint (RequestReportOrdersParams)
+ */
+export type ReportParams = {
+ from: string;
+ to: string;
+ preset?: PresetType;
+ interval: IntervalType;
+ period?: string;
+ compare_from?: string;
+ compare_to?: string;
+ compare_preset?: ComparisonPresetId;
+ comp?: '1';
+ view?: OrderAttributionView; // For order attribution reports
+ filters?: FilterCondition[];
+ section?: string;
+ date_type?: DateType; // For filtering by different date fields (created, paid, completed)
+};
+
+type PartialComparisonFields = Partial<
+ Pick< ReportParams, 'comp' | 'compare_from' | 'compare_to' >
+>;
+
+/*
+ * Checks if the comparison is present in the search params.
+ */
+/**
+ *
+ * @param p
+ */
+export function hasComparisonEnabled< T extends PartialComparisonFields >( p: T ) {
+ return p.comp === '1' && !! p.compare_from?.trim() && !! p.compare_to?.trim();
+}
+
+type NormalizeReportParamsArgType = Omit< ReportParams, 'from' | 'to' | 'interval' | 'preset' > & {
+ from?: string;
+ to?: string;
+ interval?: string;
+ preset?: PrimaryPresetId;
+};
+
+/**
+ * Returns normalized params for the report request query.
+ * When no defined, it will use the defaults.
+ *
+ * @param {NormalizeReportParamsArgType} [search] - URL search params.
+ * @param {PresetType} [defaultPreset] - Override the fallback preset.
+ */
+export function normalizeReportParams(
+ search?: NormalizeReportParamsArgType,
+ defaultPreset?: PresetType
+): ReportParams {
+ const defaults = defaultPreset
+ ? getDefaultQueryParams( true, defaultPreset )
+ : getDefaultQueryParams( true );
+
+ // Preset handling:
+ // - Use search.preset only if valid
+ // - On fresh load (no from/to), fallback to defaults.preset
+ // - If user has explicit dates but no/invalid preset,
+ // keep undefined (custom range)
+ let preset: PresetType | undefined;
+ if ( search?.preset && isSelectablePreset( search.preset ) ) {
+ preset = search.preset;
+ } else if ( ! search?.from && ! search?.to ) {
+ preset = defaults.preset;
+ }
+
+ // When a valid preset is present, recalculate from/to
+ // so rolling ranges like "Last 30 days" stay fresh
+ // on every page load instead of using stale URL dates.
+ // If the preset is valid but has no range implementation,
+ // clear it to avoid silently falling back to stale dates.
+ let presetRange: ReturnType< typeof computeDateRangeFromPreset >;
+ if ( preset ) {
+ presetRange = computeDateRangeFromPreset( preset );
+ if ( ! presetRange ) {
+ preset = undefined;
+ }
+ }
+
+ const from = presetRange?.from ?? search?.from ?? defaults.from;
+ const to = presetRange?.to ?? search?.to ?? defaults.to;
+
+ // Calculate the interval from the resolved date range.
+ const interval = getDefaultIntervalForPeriod( undefined, from, to );
+
+ // Params from `search`, or fallback to defaults.
+ const normalized: ReportParams = {
+ from,
+ to,
+ interval: interval ?? defaults.interval,
+ preset,
+ date_type: search?.date_type ?? 'created',
+ };
+
+ // Add comparison params from search if enabled
+ if ( search && hasComparisonEnabled( search ) ) {
+ normalized.compare_from = search.compare_from;
+ normalized.compare_to = search.compare_to;
+ normalized.compare_preset = search.compare_preset;
+ normalized.comp = '1';
+ } else if ( ! search?.from && hasComparisonEnabled( defaults ) ) {
+ // Fresh load (missing primary params) - apply default comparison
+ normalized.compare_from = defaults.compare_from;
+ normalized.compare_to = defaults.compare_to;
+ normalized.compare_preset = defaults.compare_preset;
+ normalized.comp = '1';
+ }
+
+ return normalized;
+}
diff --git a/projects/packages/premium-analytics/packages/data/src/utils/types.ts b/projects/packages/premium-analytics/packages/data/src/utils/types.ts
new file mode 100644
index 000000000000..35f200844148
--- /dev/null
+++ b/projects/packages/premium-analytics/packages/data/src/utils/types.ts
@@ -0,0 +1,29 @@
+/**
+ * Utility type to override properties of a type.
+ * Useful for transforming API responses where some properties change type.
+ *
+ * @example
+ * type Raw = { count: string; name: string; }
+ * type Processed = Override< Raw, { count: number } >
+ * // Result: { count: number; name: string; }
+ */
+export type Override< T, U > = Omit< T, keyof U > & U;
+
+/**
+ * Date type parameter for filtering reports by different date fields.
+ * - 'created': Filter by order creation date (date_created_gmt)
+ * - 'paid': Filter by order payment date (date_paid_gmt)
+ * - 'completed': Filter by order completion date (date_completed_gmt)
+ */
+export type DateType = 'created' | 'paid' | 'completed';
+
+/**
+ * Base parameters required by all report endpoints.
+ * These three parameters are common across all analytics reports.
+ */
+export type BaseReportParams = {
+ from: string;
+ to: string;
+ interval: string;
+ date_type?: DateType;
+};
diff --git a/projects/packages/premium-analytics/tests/jest.config.cjs b/projects/packages/premium-analytics/tests/jest.config.cjs
new file mode 100644
index 000000000000..621d39dcc87a
--- /dev/null
+++ b/projects/packages/premium-analytics/tests/jest.config.cjs
@@ -0,0 +1,13 @@
+const path = require( 'path' );
+const baseConfig = require( 'jetpack-js-tools/jest/config.base.js' );
+
+module.exports = {
+ ...baseConfig,
+ rootDir: path.join( __dirname, '..' ),
+ moduleNameMapper: {
+ ...baseConfig.moduleNameMapper,
+ // Resolve internal `packages/*` imports to their TypeScript source,
+ // mirroring the tsconfig `paths` alias (see README → "Internal packages").
+ '^@jetpack-premium-analytics/(.*)$': path.join( __dirname, '..', 'packages', '$1', 'src' ),
+ },
+};