From 5640b43a3023671bb54f07f4d91437bf75268951 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:02:28 +0800 Subject: [PATCH 1/7] feat(premium-analytics): copy data package from next-woocommerce-analytics --- .../premium-analytics/packages/data/README.md | 429 ++++++++++++++++++ .../packages/data/package.json | 23 + .../packages/data/src/api/constants.ts | 4 + .../packages/data/src/api/index.ts | 48 ++ .../src/api/report-bookings-fetch/index.ts | 5 + .../report-bookings-fetch.ts | 59 +++ .../api/report-conversion-rate-fetch/index.ts | 4 + .../report-conversion-rate-fetch.ts | 59 +++ .../api/report-coupons-by-date-fetch/index.ts | 4 + .../report-coupons-by-date-fetch.ts | 68 +++ .../src/api/report-coupons-fetch/index.ts | 4 + .../report-coupons-fetch.ts | 55 +++ .../report-customers-by-date-fetch/index.ts | 1 + .../report-customers-by-date-fetch.ts | 78 ++++ .../src/api/report-customers-fetch/index.ts | 4 + .../report-customers-fetch.ts | 57 +++ .../data/src/api/report-export-fetch/index.ts | 5 + .../report-export-fetch.ts | 61 +++ .../index.ts | 1 + ...port-order-attribution-by-product-fetch.ts | 77 ++++ .../index.ts | 6 + .../report-order-attribution-summary-fetch.ts | 90 ++++ .../data/src/api/report-orders-fetch/index.ts | 5 + .../report-orders-fetch.ts | 69 +++ .../src/api/report-products-fetch/index.ts | 7 + .../report-products-fetch.ts | 76 ++++ .../report-sessions-by-device-fetch/index.ts | 4 + .../report-sessions-by-device-fetch.ts | 64 +++ .../index.ts | 4 + .../report-visitors-by-location-fetch.ts | 61 +++ .../src/api/report-visitors-fetch/index.ts | 4 + .../report-visitors-fetch.ts | 43 ++ .../__tests__/get-default-preset.test.ts | 107 +++++ .../packages/data/src/defaults/index.ts | 1 + .../packages/data/src/defaults/reports.ts | 116 +++++ .../packages/data/src/hooks/index.ts | 12 + .../data/src/hooks/use-product-images.ts | 94 ++++ .../data/src/hooks/use-report-bookings.ts | 18 + .../src/hooks/use-report-conversion-rate.ts | 25 + .../src/hooks/use-report-coupons-by-date.ts | 17 + .../data/src/hooks/use-report-coupons.ts | 17 + .../src/hooks/use-report-customers-by-date.ts | 26 ++ .../data/src/hooks/use-report-customers.ts | 18 + .../src/hooks/use-report-order-attribution.ts | 66 +++ .../data/src/hooks/use-report-orders.ts | 26 ++ .../data/src/hooks/use-report-products.ts | 17 + .../hooks/use-report-sessions-by-device.ts | 45 ++ .../hooks/use-report-visitors-by-location.ts | 39 ++ .../data/src/hooks/use-report-visitors.ts | 26 ++ .../packages/data/src/hooks/use-report.ts | 160 +++++++ .../packages/data/src/index.ts | 53 +++ .../packages/data/src/prefetch/index.ts | 1 + .../data/src/prefetch/prefetch-report.ts | 122 +++++ .../data/src/processing/bookings/index.ts | 119 +++++ .../src/processing/conversion-rate/index.ts | 155 +++++++ .../src/processing/coupons-by-date/index.ts | 100 ++++ .../data/src/processing/coupons/index.ts | 84 ++++ .../src/processing/customers-by-date/index.ts | 179 ++++++++ .../data/src/processing/customers/index.ts | 88 ++++ .../packages/data/src/processing/index.ts | 12 + .../src/processing/order-attribution/index.ts | 2 + ...e-order-attribution-by-product-response.ts | 90 ++++ ...tize-order-attribution-summary-response.ts | 115 +++++ .../orders-by-product-type/index.ts | 82 ++++ .../data/src/processing/orders/index.ts | 81 ++++ .../data/src/processing/products/index.ts | 83 ++++ .../processing/sessions-by-device/index.ts | 83 ++++ .../packages/data/src/processing/utils.ts | 11 + .../processing/visitors-by-location/index.ts | 77 ++++ .../data/src/processing/visitors/index.ts | 83 ++++ .../src/providers/global-error-context.tsx | 114 +++++ .../src/providers/global-error-manager.ts | 35 ++ .../packages/data/src/providers/index.ts | 11 + .../src/providers/query-client-provider.tsx | 151 ++++++ .../packages/data/src/queries/index.ts | 12 + .../data/src/queries/report-bookings-query.ts | 49 ++ .../queries/report-conversion-rate-query.ts | 48 ++ .../queries/report-coupons-by-date-query.ts | 51 +++ .../data/src/queries/report-coupons-query.ts | 51 +++ .../queries/report-customers-by-date-query.ts | 50 ++ .../src/queries/report-customers-query.ts | 48 ++ .../report-order-attribution-summary-query.ts | 144 ++++++ .../data/src/queries/report-orders-query.ts | 45 ++ .../data/src/queries/report-products-query.ts | 53 +++ .../report-sessions-by-device-query.ts | 47 ++ .../report-visitors-by-location-query.ts | 46 ++ .../data/src/queries/report-visitors-query.ts | 48 ++ .../packages/data/src/types.ts | 120 +++++ .../data/src/types/filter-condition.ts | 23 + .../packages/data/src/types/product-image.ts | 7 + .../packages/data/src/types/product-type.ts | 4 + .../compute-date-range-from-preset.test.ts | 160 +++++++ .../__tests__/has-comparison-enabled.test.ts | 82 ++++ .../__tests__/normalize-report-params.test.ts | 294 ++++++++++++ .../packages/data/src/utils/date.ts | 122 +++++ .../data/src/utils/ensure-core-settings.ts | 25 + .../packages/data/src/utils/index.ts | 15 + .../packages/data/src/utils/interval.ts | 102 +++++ .../packages/data/src/utils/parsing.ts | 15 + .../data/src/utils/preset-date-range.ts | 35 ++ .../data/src/utils/product-filters.ts | 25 + .../packages/data/src/utils/search.ts | 155 +++++++ .../packages/data/src/utils/types.ts | 29 ++ .../packages/data/tsconfig.json | 9 + 104 files changed, 6154 insertions(+) create mode 100644 projects/packages/premium-analytics/packages/data/README.md create mode 100644 projects/packages/premium-analytics/packages/data/package.json create mode 100644 projects/packages/premium-analytics/packages/data/src/api/constants.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-by-date-fetch/report-coupons-by-date-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-by-date-fetch/report-customers-by-date-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-by-product-fetch/report-order-attribution-by-product-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-order-attribution-summary-fetch/report-order-attribution-summary-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-sessions-by-device-fetch/report-sessions-by-device-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-by-location-fetch/report-visitors-by-location-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/defaults/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/defaults/reports.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-conversion-rate.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-order-attribution.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-products.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/prefetch/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/order-attribution/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/products/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/utils.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/global-error-manager.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types/filter-condition.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types/product-image.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/types/product-type.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/date.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/index.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/interval.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/parsing.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/search.ts create mode 100644 projects/packages/premium-analytics/packages/data/src/utils/types.ts create mode 100644 projects/packages/premium-analytics/packages/data/tsconfig.json 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..3c6f603476e2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/README.md @@ -0,0 +1,429 @@ +# @next-woo-analytics/data + +Data management package for WooCommerce Analytics with React Query +integration. + +## Installation + +This package is an internal dependency of the WooCommerce Analytics NextAdmin integration. It's automatically available when working within the NextAdmin framework. + +```tsx +import { + AnalyticsQueryClientProvider, + useReport, + prefetchReport, + // ... other exports +} from '@next-woo-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 '@next-woo-analytics/data'; + +function App() { + return ( + + {/* Your app components */} + + ); +} +``` + +### Fetching Data + +```tsx +import { + useReportOrders, + useReportOrdersByProductType, + useReportOrderAttribution, + useReportCoupons +} from '@next-woo-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 '@next-woo-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 '@next-woo-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 '@next-woo-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 '@next-woo-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..cae8d1032019 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/package.json @@ -0,0 +1,23 @@ +{ + "name": "@next-woo-analytics/data", + "version": "1.0.0", + "type": "module", + "wpModule": true, + "main": "src/index.ts", + "exports": { + ".": "./build/src/index.js" + }, + "dependencies": { + "date-fns": "*", + "@date-fns/tz": "*", + "@tanstack/react-query": "*", + "@tanstack/react-router": "*", + "@wordpress/api-fetch": "*", + "@wordpress/url": "*", + "@next-woo-analytics/datetime": "workspace:*", + "@automattic/admin-toolkit": "*" + }, + "devDependencies": { + "@tanstack/react-query-devtools": "*" + } +} 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..6b8a8be840b6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts @@ -0,0 +1,48 @@ +/** + * Internal dependencies + */ +import type { RequestReportOrdersParams } from './report-orders-fetch'; +import type { RequestReportOrderAttributionSummaryParams } from './report-order-attribution-summary-fetch'; +import type { RequestReportOrderAttributionByProductParams } from './report-order-attribution-by-product-fetch'; +import type { RequestReportCouponsParams } from './report-coupons-fetch'; +import type { RequestReportCouponsByDateParams } from './report-coupons-by-date-fetch'; +import type { RequestReportCustomersParams } from './report-customers-fetch'; +import type { RequestReportProductsParams } from './report-products-fetch'; +import type { RequestReportVisitorsParams } from './report-visitors-fetch'; +import type { RequestReportVisitorsByLocationParams } from './report-visitors-by-location-fetch'; +import type { RequestReportBookingsParams } from './report-bookings-fetch'; +import type { RequestReportSessionsByDeviceParams } from './report-sessions-by-device-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..f85b19d565df --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-bookings-fetch/report-bookings-fetch.ts @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; + +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[]; +}; + +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..eb6a2aa2e8ed --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-conversion-rate-fetch/report-conversion-rate-fetch.ts @@ -0,0 +1,59 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import type { FilterCondition } from '../../types/filter-condition'; +import { reportsPath } from '../constants'; + +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[]; +}; + +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..0115d56b0ce6 --- /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,68 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; + +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[]; +}; + +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..4c4e60f45fd0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/index.ts @@ -0,0 +1,4 @@ +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..d591c37b1e22 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-coupons-fetch/report-coupons-fetch.ts @@ -0,0 +1,55 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; + +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[]; +}; + +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..6e6c6a60e81d --- /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,78 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +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; + +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..aca7bab2d69d --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/index.ts @@ -0,0 +1,4 @@ +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..35cdd79456dc --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-customers-fetch/report-customers-fetch.ts @@ -0,0 +1,57 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import type { FilterCondition } from '../../types/filter-condition'; +import { reportsPath } from '../constants'; + +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[]; +}; + +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..c59fd9485cc6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/index.ts @@ -0,0 +1,5 @@ +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..5edc5de7f4c7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-export-fetch/report-export-fetch.ts @@ -0,0 +1,61 @@ +/** + * 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..5e2489eb23fa --- /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,77 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +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..1ee8349c738f --- /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,90 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +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..452fd2b53371 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts @@ -0,0 +1,69 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import { hasProductFilters } from '../../utils/product-filters'; + +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[]; +}; + +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..d1d82b303a52 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/index.ts @@ -0,0 +1,7 @@ +/** + * 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..30b14661cde2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-products-fetch/report-products-fetch.ts @@ -0,0 +1,76 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; +import type { FilterCondition } from '@next-woo-analytics/data'; + +/** + * Internal dependencies + */ +import { reportsPath } from '../constants'; +import { BaseReportParams } from '../../utils/types'; + +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 + */ +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..4c28378487a5 --- /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,64 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +/** + * 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..43b5f05a1d22 --- /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,61 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +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. + */ +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..3a17020aae0c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/index.ts @@ -0,0 +1,4 @@ +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..dcf58139007c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/api/report-visitors-fetch/report-visitors-fetch.ts @@ -0,0 +1,43 @@ +/** + * External dependencies + */ +import apiFetch from '@wordpress/api-fetch'; +import { addQueryArgs } from '@wordpress/url'; + +/** + * Internal dependencies + */ +import type { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; + +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; + +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..5c39f60b76ce --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/defaults/__tests__/get-default-preset.test.ts @@ -0,0 +1,107 @@ +/** + * 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..12ad30a3b7a6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts @@ -0,0 +1,116 @@ +/** + * External dependencies + */ +import { getComparisonRangeFromPreset } from '@next-woo-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 + */ +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. + */ +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..5c5b1649f419 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-product-images.ts @@ -0,0 +1,94 @@ +/** + * 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..fc91af38a3fb --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-bookings.ts @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { reportBookingsQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +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..9bc2d545d60c --- /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; +}; + +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..6ba9daa58d92 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons-by-date.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { reportCouponsByDateQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +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..2b2fb11a3617 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-coupons.ts @@ -0,0 +1,17 @@ +/** + * Internal dependencies + */ +import { reportCouponsQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +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..1ddd8c538af0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers-by-date.ts @@ -0,0 +1,26 @@ +/** + * 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; +}; + +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..3f4a7a2aee6e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-customers.ts @@ -0,0 +1,18 @@ +/** + * Internal dependencies + */ +import { reportCustomersQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +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..7bf18f3e341e --- /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', +]; + +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..336de035aede --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-orders.ts @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { reportOrdersQuery } from '../queries'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportOrdersOptions = { + enabled?: boolean; +}; + +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..bdd30704ffe8 --- /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'; + +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..535980817370 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-sessions-by-device.ts @@ -0,0 +1,45 @@ +/** + * 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..b4252eb16676 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors-by-location.ts @@ -0,0 +1,39 @@ +/** + * 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; +}; + +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..7fd1805f2744 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report-visitors.ts @@ -0,0 +1,26 @@ +/** + * Internal dependencies + */ +import { reportVisitorsQuery } from '../queries/report-visitors-query'; +import { type ReportParams } from '../utils/search'; +import { useReport } from './use-report'; + +type UseReportVisitorsOptions = { + enabled?: boolean; +}; + +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..9d90ab78683c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts @@ -0,0 +1,160 @@ +/** + * 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..0d685aeb5de7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/index.ts @@ -0,0 +1,53 @@ +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..b5f494b174f7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts @@ -0,0 +1,122 @@ +/** + * 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 ]; +}; + +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..0d656d471de8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts @@ -0,0 +1,119 @@ +/** + * Internal dependencies + */ +import { fetchReportBookings } from '../../api/report-bookings-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseInt } from '../../utils/parsing'; + +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 + */ +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 + */ +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. + */ +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..8f715a627702 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/conversion-rate/index.ts @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import { __ } from '@wordpress/i18n'; + +/** + * Internal dependencies + */ +import { fetchReportConversionRate } from '../../api/report-conversion-rate-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseInt } from '../../utils/parsing'; + +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 + */ +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. + */ +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', 'woocommerce-analytics' ), + count: sanitizedSummary.active_sessions, + rate: 100, // Starting point + }, + { + id: 'cart-addition', + label: __( 'Cart', 'woocommerce-analytics' ), + count: sanitizedSummary.with_cart_addition, + rate: + sanitizedSummary.active_sessions > 0 + ? ( sanitizedSummary.with_cart_addition / + sanitizedSummary.active_sessions ) * + 100 + : 0, + }, + { + id: 'checkout', + label: __( 'Checkout', 'woocommerce-analytics' ), + count: sanitizedSummary.reached_checkout, + rate: + sanitizedSummary.active_sessions > 0 + ? ( sanitizedSummary.reached_checkout / + sanitizedSummary.active_sessions ) * + 100 + : 0, + }, + { + id: 'completed', + label: __( 'Purchase', 'woocommerce-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..53b5720d37a0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons-by-date/index.ts @@ -0,0 +1,100 @@ +/** + * 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[]; +}; + +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 ), + }; +} + +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. + */ +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..04c76205f2c5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts @@ -0,0 +1,84 @@ +/** + * 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 + */ +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 + */ +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. + */ +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..1c61302ae3c5 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers-by-date/index.ts @@ -0,0 +1,179 @@ +/** + * 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 + */ +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 + */ +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. + */ +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..be0fffcf7844 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts @@ -0,0 +1,88 @@ +/** + * 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 + */ +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 + */ +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. + */ +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..129d281b23a4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/normalize-order-attribution-by-product-response.ts @@ -0,0 +1,90 @@ +/** + * 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..7634d328fae4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/order-attribution/sanitize-order-attribution-summary-response.ts @@ -0,0 +1,115 @@ +/** + * Internal dependencies + */ +import { sanitizeStringNumber } from '../utils'; +import { fetchReportOrderAttributionSummary } from '../../api/report-order-attribution-summary-fetch'; + +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 + */ +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 + */ +function sanitizeOrderAttributionPeriod( + period: OrderAttributionPeriod +): SanitizedOrderAttributionPeriod { + return { + value: sanitizeStringNumber( period.value ), + intervals: period.intervals.map( sanitizeOrderAttributionInterval ), + }; +} + +/** + * Sanitizes a single order attribution summary 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..faf1f71af07e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders-by-product-type/index.ts @@ -0,0 +1,82 @@ +/** + * Internal dependencies + */ +import type { + ReportsOrdersByDateResponse, + RequestReportOrdersParams, +} from '../../api/report-orders-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseFloat, safeParseInt } from '../../utils/parsing'; + +/** + * 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 + */ +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. + */ +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..32dc9661797b --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts @@ -0,0 +1,81 @@ +/** + * Internal dependencies + */ +import { fetchReportOrders } from '../../api/report-orders-fetch'; +import type { Override } from '../../utils/types'; +import { safeParseFloat, safeParseInt } from '../../utils/parsing'; + +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 + */ +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. + */ +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..a3937da22a13 --- /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 + */ +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 ), + }; +} + +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. + */ +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..f88fc25fc0c4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/sessions-by-device/index.ts @@ -0,0 +1,83 @@ +/** + * 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..509856885bc9 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors-by-location/index.ts @@ -0,0 +1,77 @@ +/** + * 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; + } +>; + +function sanitizeVisitorsByLocationItem( + item: RawVisitorsByLocationItem +): SanitizedVisitorsByLocationItem { + const visitors = Number.parseInt( item.visitors, 10 ); + + return { + ...item, + visitors: Number.isNaN( visitors ) ? 0 : visitors, + }; +} + +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..3a787c37cba6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts @@ -0,0 +1,83 @@ +/** + * 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 + */ +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. + */ +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..1a03b0e0dd64 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/global-error-context.tsx @@ -0,0 +1,114 @@ +/** + * External dependencies + */ +import { + createContext, + useContext, + useEffect, + useMemo, + useSyncExternalStore, + type ReactNode, +} from 'react'; +import { onlineManager } from '@tanstack/react-query'; + +/** + * 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. + */ +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..5cc52b9b003a --- /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..641d073e3b48 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/index.ts @@ -0,0 +1,11 @@ +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..273020c75201 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/providers/query-client-provider.tsx @@ -0,0 +1,151 @@ +/** + * External dependencies + */ +import { + QueryClient, + QueryClientProvider, + QueryCache, +} from '@tanstack/react-query'; +import { useExperiments } from '@automattic/admin-toolkit'; +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; + +const ReactQueryDevtoolsProduction = lazy( () => + // eslint-disable-next-line import/no-extraneous-dependencies -- DevTools is intentionally in devDependencies, only loaded when enabled + 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. + */ +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; +} ) => { + const { enabledExperiments } = useExperiments(); + return ( + + <>{ children } + { enabledExperiments[ 'tanstack/query-dev-tool' ] && ( + + + + ) } + + ); +}; 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..f1a4cdf921c4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-bookings-query.ts @@ -0,0 +1,49 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportBookings } from '../api'; +import { sanitizeReportBookingsResponse } from '../processing/bookings'; +import type { ReportDataMap } from '../types'; + +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; + +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 + */ + 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..5133245335d3 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-conversion-rate-query.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * 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'; + +const getReportConversionRateQueryKey = ( + p: RequestReportConversionRateParams +) => + [ + 'reports', + 'conversion-rate', + p.from, + p.to, + p.interval, + p.date_type, + p.filters, + ] as const; + +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 + */ + 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..fe9e4d09b18e --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-by-date-query.ts @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCouponsByDate } from '../api'; +import { sanitizeReportCouponsByDateResponse } from '../processing/coupons-by-date'; +import type { ReportDataMap } from '../types'; +import { FilterCondition } from '../types/filter-condition'; + +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; + +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 + */ + 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..05244b7017ba --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-coupons-query.ts @@ -0,0 +1,51 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCoupons } from '../api'; +import { sanitizeReportCouponsResponse } from '../processing/coupons'; +import type { ReportDataMap } from '../types'; +import { FilterCondition } from '../types/filter-condition'; + +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; + +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 + */ + 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..b030320e91e4 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-by-date-query.ts @@ -0,0 +1,50 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCustomersByDate } from '../api/report-customers-by-date-fetch'; +import { sanitizeReportCustomersByDateResponse } from '../processing/customers-by-date'; +import type { ReportDataMap } from '../types'; + +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; + +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 + */ + 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..c214fd28c6ed --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-customers-query.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportCustomers } from '../api'; +import { sanitizeReportCustomersResponse } from '../processing/customers'; +import type { ReportDataMap } from '../types'; + +type RequestReportCustomersParams = Parameters< + typeof fetchReportCustomers +>[ 0 ]; + +const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [ + 'reports', + 'customers', + 'new-returning', + p.from, + p.to, + p.date_type, + p.filters, +]; + +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 + */ + 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..deda11889eaf --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-order-attribution-summary-query.ts @@ -0,0 +1,144 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { + fetchReportOrderAttributionSummary, + fetchReportOrderAttributionByProduct, +} from '../api'; +import { + sanitizeReportOrderAttributionSummaryResponse, + normalizeOrderAttributionByProductResponse, + type SanitizedOrderAttributionSummaryResponse, +} from '../processing/order-attribution'; +import type { FilterCondition } from '../types/filter-condition'; +import { hasProductFilters } from '../utils/product-filters'; + +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. + */ +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. + */ + 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..50703f37728c --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-orders-query.ts @@ -0,0 +1,45 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportOrders } from '../api'; +import { sanitizeReportOrdersResponse } from '../processing/orders'; +import type { ReportDataMap } from '../types'; + +type RequestReportOrdersParams = Parameters< typeof fetchReportOrders >[ 0 ]; + +const getReportOrdersQueryKey = ( p: RequestReportOrdersParams ) => [ + 'reports', + 'orders', + p.from, + p.to, + p.interval, + p.date_type, + p.filters || [], +]; + +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 + */ + 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..4696117be2e2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-products-query.ts @@ -0,0 +1,53 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportProducts } from '../api/report-products-fetch'; +import { sanitizeReportProductsResponse } from '../processing/products'; + +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; + +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 + */ + 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..8e425b7e7127 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-sessions-by-device-query.ts @@ -0,0 +1,47 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportSessionsByDevice } from '../api/report-sessions-by-device-fetch'; +import { sanitizeReportSessionsByDeviceResponse } from '../processing/sessions-by-device'; +import type { ReportDataMap } from '../types'; + +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 + */ + 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..a75988200065 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-by-location-query.ts @@ -0,0 +1,46 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportVisitorsByLocation } from '../api'; +import { sanitizeReportVisitorsByLocationResponse } from '../processing/visitors-by-location'; +import type { ReportDataMap } from '../types'; + +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; + +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..d3749c184256 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/queries/report-visitors-query.ts @@ -0,0 +1,48 @@ +/** + * External dependencies + */ +import type { UseQueryOptions } from '@tanstack/react-query'; + +/** + * Internal dependencies + */ +import { fetchReportVisitors } from '../api'; +import { sanitizeReportVisitorsResponse } from '../processing/visitors'; +import type { ReportDataMap } from '../types'; + +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; + +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 + */ + 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..8ad61e0cbbd0 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/types.ts @@ -0,0 +1,120 @@ +/** + * Internal dependencies + */ +import { + sanitizeReportOrdersResponse, + sanitizeReportProductsResponse, +} from './processing'; +import { sanitizeReportCustomersResponse } from './processing/customers'; +import { sanitizeReportCustomersByDateResponse } from './processing/customers-by-date'; +import { sanitizeReportOrderAttributionSummaryResponse } from './processing/order-attribution'; +import { sanitizeReportCouponsResponse } from './processing/coupons'; +import { sanitizeReportCouponsByDateResponse } from './processing/coupons-by-date'; +import { sanitizeReportVisitorsResponse } from './processing/visitors'; +import { sanitizeReportVisitorsByLocationResponse } from './processing/visitors-by-location'; +import { sanitizeReportConversionRateResponse } from './processing/conversion-rate'; +import { sanitizeReportOrdersByProductTypeResponse } from './processing/orders-by-product-type'; +import { sanitizeReportBookingsResponse } from './processing/bookings'; +import { sanitizeReportSessionsByDeviceResponse } from './processing/sessions-by-device'; +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..2ad7b3fb685d --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/compute-date-range-from-preset.test.ts @@ -0,0 +1,160 @@ +/** + * External dependencies + */ +import { + startOfDay, + endOfDay, + subDays, + subMonths, + subYears, + startOfMonth, + endOfMonth, + startOfYear, + endOfYear, +} from 'date-fns'; +import { tz } from '@date-fns/tz'; + +/** + * 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. + */ +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..88ca70758db1 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/has-comparison-enabled.test.ts @@ -0,0 +1,82 @@ +/** + * 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..25850845d3c8 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/__tests__/normalize-report-params.test.ts @@ -0,0 +1,294 @@ +/** + * 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 { normalizeReportParams } from '../search'; +import { getDefaultQueryParams } from '../../defaults'; +import { computeDateRangeFromPreset } from '../preset-date-range'; +import { getDefaultIntervalForPeriod } from '../interval'; +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..41f385c01ab7 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts @@ -0,0 +1,122 @@ +/** + * External dependencies + */ +import { select } from '@wordpress/data'; +import { store as coreStore, type Settings } from '@wordpress/core-data'; +import { + toLocalTZ, + formatToTimezoneNaiveString as _formatNaive, + dateToISOStringWithTZ as _toISOWithTZ, +} from '@next-woo-analytics/datetime'; +import { type TZDate } from '@date-fns/tz'; + +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) + */ +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" + */ +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" + */ +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..697202c07c86 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/ensure-core-settings.ts @@ -0,0 +1,25 @@ +/** + * External dependencies + */ +import { resolveSelect } from '@wordpress/data'; +import { store as coreStore } from '@wordpress/core-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..1883dee36011 --- /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 '@next-woo-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..e73a9a4e04b2 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts @@ -0,0 +1,102 @@ +/** + * External dependencies + */ +import { differenceInHours } from 'date-fns'; + +/** + * Internal dependencies + */ +import type { IntervalType } from './search'; +import { localTZDate } from './date'; + +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. + * + * @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 ); + } +} + +export function getDefaultIntervalForPeriod( + period: string | undefined, + from: string, + to: string +): IntervalType { + return getAllowedIntervalsForPeriod( period, from, to )?.[ 0 ] ?? 'day'; +} + +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..3ff816683937 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts @@ -0,0 +1,15 @@ +/** + * Safe integer parsing with fallback value + */ +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 + */ +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..6e0ad5b9c023 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/preset-date-range.ts @@ -0,0 +1,35 @@ +/** + * External dependencies + */ +import { computePrimaryRange } from '@next-woo-analytics/datetime'; +import type { SelectablePresetId } from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date'; + +/** + * 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..7c000b4ea339 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts @@ -0,0 +1,25 @@ +/** + * 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..12c2869640d6 --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts @@ -0,0 +1,155 @@ +/** + * External dependencies + */ +import { + isSelectablePreset, + type SelectablePresetId, + type ComparisonPresetId, + type PrimaryPresetId, +} from '@next-woo-analytics/datetime'; + +/** + * Internal dependencies + */ +import { getDefaultQueryParams } from '../defaults'; +import { ORDER_ATTRIBUTION_VIEWS } from '../api/report-order-attribution-summary-fetch'; +import { getDefaultIntervalForPeriod } from './interval'; +import { computeDateRangeFromPreset } from './preset-date-range'; +import type { FilterCondition } from '../types/filter-condition'; +import type { DateType } from './types'; + +export type { FilterCondition }; + +/** + * Re-export SelectablePresetId as PresetType for backward compatibility. + * The canonical type now lives in @next-woo-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. + */ +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/packages/data/tsconfig.json b/projects/packages/premium-analytics/packages/data/tsconfig.json new file mode 100644 index 000000000000..3d73602e2edb --- /dev/null +++ b/projects/packages/premium-analytics/packages/data/tsconfig.json @@ -0,0 +1,9 @@ +{ + "extends": "../../tsconfig.json", + "compilerOptions": { + "outDir": "build", + "declaration": true, + "declarationDir": "build" + }, + "include": ["src/**/*"] +} \ No newline at end of file From f4f06289417f76a466721f73fa3d530accb2f4a0 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:04:12 +0800 Subject: [PATCH 2/7] refactor(premium-analytics): adapt data package imports and manifest for monorepo --- .../premium-analytics/packages/data/README.md | 24 ++++++++------- .../packages/data/package.json | 30 +++++++++---------- .../report-products-fetch.ts | 2 +- .../packages/data/src/defaults/reports.ts | 2 +- .../src/processing/conversion-rate/index.ts | 8 ++--- .../packages/data/src/utils/date.ts | 2 +- .../packages/data/src/utils/index.ts | 2 +- .../data/src/utils/preset-date-range.ts | 4 +-- .../packages/data/src/utils/search.ts | 4 +-- .../packages/data/tsconfig.json | 9 ------ 10 files changed, 40 insertions(+), 47 deletions(-) delete mode 100644 projects/packages/premium-analytics/packages/data/tsconfig.json diff --git a/projects/packages/premium-analytics/packages/data/README.md b/projects/packages/premium-analytics/packages/data/README.md index 3c6f603476e2..7db93ce258ff 100644 --- a/projects/packages/premium-analytics/packages/data/README.md +++ b/projects/packages/premium-analytics/packages/data/README.md @@ -1,11 +1,13 @@ -# @next-woo-analytics/data +# @jetpack-premium-analytics/data -Data management package for WooCommerce Analytics with React Query -integration. +Data management for Jetpack Premium Analytics with React Query integration. ## Installation -This package is an internal dependency of the WooCommerce Analytics NextAdmin integration. It's automatically available when working within the NextAdmin framework. +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 { @@ -13,7 +15,7 @@ import { useReport, prefetchReport, // ... other exports -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; ``` ## Features @@ -34,7 +36,7 @@ import { ### Setup ```tsx -import { AnalyticsQueryClientProvider } from '@next-woo-analytics/data'; +import { AnalyticsQueryClientProvider } from '@jetpack-premium-analytics/data'; function App() { return ( @@ -53,7 +55,7 @@ import { useReportOrdersByProductType, useReportOrderAttribution, useReportCoupons -} from '@next-woo-analytics/data'; +} from '@jetpack-premium-analytics/data'; function OrdersReport() { // Orders endpoint separates primary and comparison periods @@ -103,7 +105,7 @@ function CouponsReport() { ### Prefetching ```tsx -import { prefetchReport, ensureCoreSettingsReady } from '@next-woo-analytics/data'; +import { prefetchReport, ensureCoreSettingsReady } from '@jetpack-premium-analytics/data'; export const route = { beforeLoad: async () => { @@ -239,7 +241,7 @@ Returns the optimal default interval for a given time period. **Example:** ```tsx -import { getDefaultIntervalForPeriod } from '@next-woo-analytics/data'; +import { getDefaultIntervalForPeriod } from '@jetpack-premium-analytics/data'; const interval = getDefaultIntervalForPeriod( 'last-7-days', from, to ); // Returns 'day' ``` @@ -252,7 +254,7 @@ Constant array of available order attribution views. **Example:** ```tsx -import { ORDER_ATTRIBUTION_VIEWS } from '@next-woo-analytics/data'; +import { ORDER_ATTRIBUTION_VIEWS } from '@jetpack-premium-analytics/data'; // Use in components for view selection const views = ORDER_ATTRIBUTION_VIEWS; // ['channel', 'source', ...] @@ -331,7 +333,7 @@ Creates a timezone-aware date using the site's configured timezone by default. ```typescript -import { localTZDate } from '@next-woo-analytics/data'; +import { localTZDate } from '@jetpack-premium-analytics/data'; const now = localTZDate(); // Current time in site timezone const custom = localTZDate( '2024-01-15', 'America/New_York' ); diff --git a/projects/packages/premium-analytics/packages/data/package.json b/projects/packages/premium-analytics/packages/data/package.json index cae8d1032019..74470af6fbd8 100644 --- a/projects/packages/premium-analytics/packages/data/package.json +++ b/projects/packages/premium-analytics/packages/data/package.json @@ -1,23 +1,23 @@ { - "name": "@next-woo-analytics/data", - "version": "1.0.0", + "name": "@automattic/jetpack-premium-analytics-data", + "version": "0.1.0", + "private": true, "type": "module", - "wpModule": true, "main": "src/index.ts", - "exports": { - ".": "./build/src/index.js" - }, + "types": "src/index.ts", + "sideEffects": false, "dependencies": { - "date-fns": "*", - "@date-fns/tz": "*", - "@tanstack/react-query": "*", - "@tanstack/react-router": "*", - "@wordpress/api-fetch": "*", - "@wordpress/url": "*", - "@next-woo-analytics/datetime": "workspace:*", - "@automattic/admin-toolkit": "*" + "@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": "*" + "@tanstack/react-query-devtools": "5.90.2" } } 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 index 30b14661cde2..d05c09bae09d 100644 --- 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 @@ -3,13 +3,13 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; -import type { FilterCondition } from '@next-woo-analytics/data'; /** * Internal dependencies */ import { reportsPath } from '../constants'; import { BaseReportParams } from '../../utils/types'; +import type { FilterCondition } from '../../types/filter-condition'; export type RequestReportProductsParams = Omit< BaseReportParams, diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts index 12ad30a3b7a6..8ea911e54ec6 100644 --- a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts +++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts @@ -1,7 +1,7 @@ /** * External dependencies */ -import { getComparisonRangeFromPreset } from '@next-woo-analytics/datetime'; +import { getComparisonRangeFromPreset } from '@jetpack-premium-analytics/datetime'; import { differenceInCalendarDays, startOfDay } from 'date-fns'; /** 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 index 8f715a627702..3e33c27c69af 100644 --- 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 @@ -105,13 +105,13 @@ export const sanitizeReportConversionRateResponse = ( const steps: FunnelStep[] = [ { id: 'sessions', - label: __( 'Sessions', 'woocommerce-analytics' ), + label: __( 'Sessions', 'jetpack-premium-analytics' ), count: sanitizedSummary.active_sessions, rate: 100, // Starting point }, { id: 'cart-addition', - label: __( 'Cart', 'woocommerce-analytics' ), + label: __( 'Cart', 'jetpack-premium-analytics' ), count: sanitizedSummary.with_cart_addition, rate: sanitizedSummary.active_sessions > 0 @@ -122,7 +122,7 @@ export const sanitizeReportConversionRateResponse = ( }, { id: 'checkout', - label: __( 'Checkout', 'woocommerce-analytics' ), + label: __( 'Checkout', 'jetpack-premium-analytics' ), count: sanitizedSummary.reached_checkout, rate: sanitizedSummary.active_sessions > 0 @@ -133,7 +133,7 @@ export const sanitizeReportConversionRateResponse = ( }, { id: 'completed', - label: __( 'Purchase', 'woocommerce-analytics' ), + label: __( 'Purchase', 'jetpack-premium-analytics' ), count: sanitizedSummary.completed_checkout, rate: sanitizedSummary.active_sessions > 0 diff --git a/projects/packages/premium-analytics/packages/data/src/utils/date.ts b/projects/packages/premium-analytics/packages/data/src/utils/date.ts index 41f385c01ab7..011e8ae2b768 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/date.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts @@ -7,7 +7,7 @@ import { toLocalTZ, formatToTimezoneNaiveString as _formatNaive, dateToISOStringWithTZ as _toISOWithTZ, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; import { type TZDate } from '@date-fns/tz'; type FullSettings = Settings & { diff --git a/projects/packages/premium-analytics/packages/data/src/utils/index.ts b/projects/packages/premium-analytics/packages/data/src/utils/index.ts index 1883dee36011..0a0763050244 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/index.ts @@ -11,5 +11,5 @@ 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 '@next-woo-analytics/datetime'; +export { isSelectablePreset } from '@jetpack-premium-analytics/datetime'; export type { Override, BaseReportParams } from './types'; 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 index 6e0ad5b9c023..d3695405408c 100644 --- 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 @@ -1,8 +1,8 @@ /** * External dependencies */ -import { computePrimaryRange } from '@next-woo-analytics/datetime'; -import type { SelectablePresetId } from '@next-woo-analytics/datetime'; +import { computePrimaryRange } from '@jetpack-premium-analytics/datetime'; +import type { SelectablePresetId } from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies diff --git a/projects/packages/premium-analytics/packages/data/src/utils/search.ts b/projects/packages/premium-analytics/packages/data/src/utils/search.ts index 12c2869640d6..4d01bd951f09 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/search.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts @@ -6,7 +6,7 @@ import { type SelectablePresetId, type ComparisonPresetId, type PrimaryPresetId, -} from '@next-woo-analytics/datetime'; +} from '@jetpack-premium-analytics/datetime'; /** * Internal dependencies @@ -22,7 +22,7 @@ export type { FilterCondition }; /** * Re-export SelectablePresetId as PresetType for backward compatibility. - * The canonical type now lives in @next-woo-analytics/datetime. + * The canonical type now lives in @jetpack-premium-analytics/datetime. */ export type PresetType = SelectablePresetId; diff --git a/projects/packages/premium-analytics/packages/data/tsconfig.json b/projects/packages/premium-analytics/packages/data/tsconfig.json deleted file mode 100644 index 3d73602e2edb..000000000000 --- a/projects/packages/premium-analytics/packages/data/tsconfig.json +++ /dev/null @@ -1,9 +0,0 @@ -{ - "extends": "../../tsconfig.json", - "compilerOptions": { - "outDir": "build", - "declaration": true, - "declarationDir": "build" - }, - "include": ["src/**/*"] -} \ No newline at end of file From 8510450d21b63e11f489c7272c4fb68f25a5ee35 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:04:48 +0800 Subject: [PATCH 3/7] refactor(premium-analytics): decouple data package from admin-toolkit experiments --- .../src/providers/query-client-provider.tsx | 26 ++++++++++++++++--- 1 file changed, 23 insertions(+), 3 deletions(-) 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 index 273020c75201..c6ddf592efbc 100644 --- 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 @@ -6,7 +6,6 @@ import { QueryClientProvider, QueryCache, } from '@tanstack/react-query'; -import { useExperiments } from '@automattic/admin-toolkit'; import { ReactNode, lazy, Suspense } from 'react'; /** @@ -17,6 +16,28 @@ 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( () => // eslint-disable-next-line import/no-extraneous-dependencies -- DevTools is intentionally in devDependencies, only loaded when enabled import( '@tanstack/react-query-devtools/production' ).then( ( d ) => ( { @@ -137,11 +158,10 @@ export const AnalyticsQueryClientProvider = ( { }: { children: ReactNode; } ) => { - const { enabledExperiments } = useExperiments(); return ( <>{ children } - { enabledExperiments[ 'tanstack/query-dev-tool' ] && ( + { areQueryDevtoolsEnabled() && ( From cacb76f75690468bbf01a91498a523972b0f6b89 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:16:16 +0800 Subject: [PATCH 4/7] style(premium-analytics): align ported data package with jetpack lint and prettier --- .../packages/data/src/api/index.ts | 19 ++--- .../report-bookings-fetch.ts | 12 ++- .../report-conversion-rate-fetch.ts | 13 ++- .../report-coupons-by-date-fetch.ts | 12 ++- .../src/api/report-coupons-fetch/index.ts | 5 +- .../report-coupons-fetch.ts | 12 ++- .../report-customers-by-date-fetch.ts | 11 ++- .../src/api/report-customers-fetch/index.ts | 5 +- .../report-customers-fetch.ts | 18 ++-- .../data/src/api/report-export-fetch/index.ts | 5 +- .../report-export-fetch.ts | 10 +-- ...port-order-attribution-by-product-fetch.ts | 3 +- .../report-order-attribution-summary-fetch.ts | 11 +-- .../report-orders-fetch.ts | 14 +++- .../src/api/report-products-fetch/index.ts | 5 +- .../report-products-fetch.ts | 9 +- .../report-sessions-by-device-fetch.ts | 8 +- .../report-visitors-by-location-fetch.ts | 10 ++- .../src/api/report-visitors-fetch/index.ts | 5 +- .../report-visitors-fetch.ts | 10 ++- .../__tests__/get-default-preset.test.ts | 29 ++----- .../packages/data/src/defaults/reports.ts | 10 +-- .../data/src/hooks/use-product-images.ts | 8 +- .../data/src/hooks/use-report-bookings.ts | 14 ++-- .../src/hooks/use-report-conversion-rate.ts | 14 ++-- .../src/hooks/use-report-coupons-by-date.ts | 13 ++- .../data/src/hooks/use-report-coupons.ts | 13 ++- .../src/hooks/use-report-customers-by-date.ts | 15 ++-- .../data/src/hooks/use-report-customers.ts | 6 +- .../src/hooks/use-report-order-attribution.ts | 12 +-- .../data/src/hooks/use-report-orders.ts | 20 ++--- .../data/src/hooks/use-report-products.ts | 14 ++-- .../hooks/use-report-sessions-by-device.ts | 10 +-- .../hooks/use-report-visitors-by-location.ts | 15 ++-- .../data/src/hooks/use-report-visitors.ts | 20 ++--- .../packages/data/src/hooks/use-report.ts | 12 +-- .../packages/data/src/index.ts | 20 +---- .../data/src/prefetch/prefetch-report.ts | 53 ++++-------- .../data/src/processing/bookings/index.ts | 40 +++------ .../src/processing/conversion-rate/index.ts | 31 +++---- .../src/processing/coupons-by-date/index.ts | 17 ++-- .../data/src/processing/coupons/index.ts | 15 ++-- .../src/processing/customers-by-date/index.ts | 68 +++++---------- .../data/src/processing/customers/index.ts | 17 ++-- ...e-order-attribution-by-product-response.ts | 28 +++---- ...tize-order-attribution-summary-response.ts | 5 +- .../orders-by-product-type/index.ts | 4 +- .../data/src/processing/orders/index.ts | 12 ++- .../data/src/processing/products/index.ts | 18 ++-- .../processing/sessions-by-device/index.ts | 18 ++-- .../processing/visitors-by-location/index.ts | 23 +++--- .../data/src/processing/visitors/index.ts | 13 ++- .../src/providers/global-error-context.tsx | 20 ++--- .../src/providers/global-error-manager.ts | 2 +- .../packages/data/src/providers/index.ts | 10 +-- .../src/providers/query-client-provider.tsx | 19 ++--- .../data/src/queries/report-bookings-query.ts | 24 ++---- .../queries/report-conversion-rate-query.ts | 27 +++--- .../queries/report-coupons-by-date-query.ts | 25 +++--- .../data/src/queries/report-coupons-query.ts | 25 +++--- .../queries/report-customers-by-date-query.ts | 27 +++--- .../src/queries/report-customers-query.ts | 13 +-- .../report-order-attribution-summary-query.ts | 82 ++++++++----------- .../data/src/queries/report-orders-query.ts | 9 +- .../data/src/queries/report-products-query.ts | 17 ++-- .../report-sessions-by-device-query.ts | 14 ++-- .../report-visitors-by-location-query.ts | 12 +-- .../data/src/queries/report-visitors-query.ts | 23 ++---- .../packages/data/src/types.ts | 53 ++++-------- .../compute-date-range-from-preset.test.ts | 32 +++----- .../__tests__/has-comparison-enabled.test.ts | 3 +- .../__tests__/normalize-report-params.test.ts | 9 +- .../packages/data/src/utils/date.ts | 54 ++++++------ .../data/src/utils/ensure-core-settings.ts | 8 +- .../packages/data/src/utils/interval.ts | 32 ++++++-- .../packages/data/src/utils/parsing.ts | 4 + .../data/src/utils/preset-date-range.ts | 3 +- .../data/src/utils/product-filters.ts | 4 +- .../packages/data/src/utils/search.ts | 36 +++----- 79 files changed, 584 insertions(+), 807 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/src/api/index.ts b/projects/packages/premium-analytics/packages/data/src/api/index.ts index 6b8a8be840b6..771d51ddba7b 100644 --- a/projects/packages/premium-analytics/packages/data/src/api/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/api/index.ts @@ -1,17 +1,17 @@ /** * Internal dependencies */ -import type { RequestReportOrdersParams } from './report-orders-fetch'; -import type { RequestReportOrderAttributionSummaryParams } from './report-order-attribution-summary-fetch'; -import type { RequestReportOrderAttributionByProductParams } from './report-order-attribution-by-product-fetch'; -import type { RequestReportCouponsParams } from './report-coupons-fetch'; +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 { RequestReportVisitorsParams } from './report-visitors-fetch'; -import type { RequestReportVisitorsByLocationParams } from './report-visitors-by-location-fetch'; -import type { RequestReportBookingsParams } from './report-bookings-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 & @@ -42,7 +42,4 @@ export { fetchReportVisitorsByLocation } from './report-visitors-by-location-fet 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'; +export type { ExportReportParams, ExportReportResponse } from './report-export-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 index f85b19d565df..c06c8fa36fa2 100644 --- 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 @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type ReportsBookingsByDateSummary = { status_unpaid: string; @@ -38,6 +37,15 @@ 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, 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 index eb6a2aa2e8ed..eaea267f5bdc 100644 --- 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 @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; -import type { FilterCondition } from '../../types/filter-condition'; import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type ReportsConversionRateByDateSummary = { active_sessions: string; @@ -40,6 +39,14 @@ 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, 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 index 0115d56b0ce6..d58602ec6645 100644 --- 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 @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type CouponsByDateDataItem = { time_interval: string; @@ -49,6 +48,15 @@ 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, 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 index 4c4e60f45fd0..54bbee440ef2 100644 --- 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 @@ -1,4 +1 @@ -export { - fetchReportCoupons, - type RequestReportCouponsParams, -} from './report-coupons-fetch'; +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 index d591c37b1e22..c25dc9dd29c0 100644 --- 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 @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type CouponsDataItem = { coupon_code: string; @@ -35,6 +34,15 @@ 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, 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 index 6e6c6a60e81d..9d9e83b66d5d 100644 --- 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 @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; type ReportsCustomersByDateSummary = { total_net_sales: string; @@ -61,6 +60,14 @@ type ReportsCustomersByDateResponse = { 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, 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 index aca7bab2d69d..b5d7c2f32fe4 100644 --- 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 @@ -1,4 +1 @@ -export { - fetchReportCustomers, - type RequestReportCustomersParams, -} from './report-customers-fetch'; +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 index 35cdd79456dc..1747d7c8cdb4 100644 --- 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 @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; -import type { FilterCondition } from '../../types/filter-condition'; import { reportsPath } from '../constants'; +import type { FilterCondition } from '../../types/filter-condition'; +import type { BaseReportParams } from '../../utils/types'; type CustomersNewReturningSummary = { total_net_sales: string; @@ -31,13 +30,18 @@ type ReportsCustomersNewReturningResponse = { data: CustomersNewReturningItem[]; }; -export type RequestReportCustomersParams = Omit< - BaseReportParams, - 'interval' -> & { +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, 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 index c59fd9485cc6..4b36b16295d5 100644 --- 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 @@ -1,5 +1,2 @@ export { exportReport } from './report-export-fetch'; -export type { - ExportReportParams, - ExportReportResponse, -} 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 index 5edc5de7f4c7..645bdff72b3f 100644 --- 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 @@ -29,18 +29,14 @@ export interface ExportReportResponse { /** * Export one or more reports via email * - * @param params Export parameters + * @param params - Export parameters * @return Promise that resolves to the export response */ -export async function exportReport( - params: ExportReportParams -): Promise< ExportReportResponse > { +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 ], + report_type: Array.isArray( params.reportType ) ? params.reportType : [ params.reportType ], from: params.from, to: params.to, interval: params.interval || 'day', 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 index 5e2489eb23fa..7e35aff2130e 100644 --- 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 @@ -3,13 +3,12 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; 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 ]; 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 index 1ee8349c738f..6c62c763f539 100644 --- 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 @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; export const ORDER_ATTRIBUTION_VIEWS = [ 'channel', @@ -64,8 +63,7 @@ export type RequestReportOrderAttributionSummaryParams = BaseReportParams & { export async function fetchReportOrderAttributionSummary( params: RequestReportOrderAttributionSummaryParams ): Promise< OrderAttributionSummaryResponse > { - const { from, to, interval, view, compare_from, compare_to, date_type } = - params; + const { from, to, interval, view, compare_from, compare_to, date_type } = params; /* * Order attribution endpoint requires compare_from and compare_to. @@ -81,10 +79,7 @@ export async function fetchReportOrderAttributionSummary( date_type, }; - const path = addQueryArgs( - `${ reportsPath }/order-attribution/${ view }/summary`, - queryParams - ); + 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/report-orders-fetch.ts b/projects/packages/premium-analytics/packages/data/src/api/report-orders-fetch/report-orders-fetch.ts index 452fd2b53371..c97e7f3d8aa9 100644 --- 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 @@ -3,14 +3,13 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; +import { hasProductFilters } from '../../utils/product-filters'; import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; -import { hasProductFilters } from '../../utils/product-filters'; +import type { BaseReportParams } from '../../utils/types'; type ReportsOrdersByDateSummary = { average_order_value: string; @@ -45,6 +44,15 @@ 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, 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 index d1d82b303a52..c786309418cf 100644 --- 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 @@ -1,7 +1,4 @@ /** * Internal dependencies */ -export { - fetchReportProducts, - type RequestReportProductsParams, -} from './report-products-fetch'; +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 index d05c09bae09d..8af88655042d 100644 --- 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 @@ -3,18 +3,14 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import { reportsPath } from '../constants'; import { BaseReportParams } from '../../utils/types'; +import { reportsPath } from '../constants'; import type { FilterCondition } from '../../types/filter-condition'; -export type RequestReportProductsParams = Omit< - BaseReportParams, - 'interval' -> & { +export type RequestReportProductsParams = Omit< BaseReportParams, 'interval' > & { limit?: number; orderby?: string; order?: 'asc' | 'desc'; @@ -43,6 +39,7 @@ type ReportProductsResponse = { /** * Fetches products report data from the WooCommerce Analytics API + * @param params */ export async function fetchReportProducts( params: RequestReportProductsParams 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 index 4c28378487a5..3909742411dc 100644 --- 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 @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; /** * Raw response item from the sessions/by-device endpoint. @@ -36,10 +35,7 @@ type ReportsSessionsByDeviceResponse = { data: SessionsByDeviceItem[]; }; -export type RequestReportSessionsByDeviceParams = Omit< - BaseReportParams, - 'interval' ->; +export type RequestReportSessionsByDeviceParams = Omit< BaseReportParams, 'interval' >; /** * Fetch sessions by device type report data. 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 index 43b5f05a1d22..bbb9c7fa6193 100644 --- 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 @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; type VisitorsByLocationReportDataItem = { country_code: string; @@ -39,6 +38,13 @@ export type RequestReportVisitorsByLocationParams = BaseReportParams & { * * 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, 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 index 3a17020aae0c..164ef5351eb7 100644 --- 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 @@ -1,4 +1 @@ -export { - fetchReportVisitors, - type RequestReportVisitorsParams, -} from './report-visitors-fetch'; +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 index dcf58139007c..93895fca50cd 100644 --- 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 @@ -3,12 +3,11 @@ */ import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ -import type { BaseReportParams } from '../../utils/types'; import { reportsPath } from '../constants'; +import type { BaseReportParams } from '../../utils/types'; type ReportsVisitorsByDateSummary = { active_sessions: string; @@ -28,6 +27,13 @@ type ReportsVisitorsByDateResponse = { export type RequestReportVisitorsParams = BaseReportParams; +/** + * + * @param root0 + * @param root0.from + * @param root0.to + * @param root0.interval + */ export async function fetchReportVisitors( { from, to, 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 index 5c39f60b76ce..ab6da5ee50fb 100644 --- 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 @@ -16,7 +16,6 @@ jest.mock( '@wordpress/data', () => ( { jest.mock( '../../utils/ensure-core-settings', () => ( { ensureCoreSettingsReady: jest.fn( () => Promise.resolve() ), } ) ); - /** * Internal dependencies */ @@ -37,21 +36,15 @@ describe( 'getDefaultQueryParams - preset override', () => { } ); it( 'uses today preset when passed', () => { - expect( getDefaultQueryParams( false, 'today' ).preset ).toBe( - 'today' - ); + 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' - ); + 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' - ); + expect( getDefaultQueryParams( false, 'last-30-days' ).preset ).toBe( 'last-30-days' ); } ); } ); @@ -78,27 +71,19 @@ describe( 'getDefaultPreset', () => { } ); it( 'returns last-7-days when launched 3 days ago', () => { - expect( getDefaultPreset( '2025-03-12T00:00:00Z' ) ).toBe( - 'last-7-days' - ); + 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' - ); + 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' - ); + 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' - ); + expect( getDefaultPreset( '2024-01-01T00:00:00Z' ) ).toBe( 'last-30-days' ); } ); it( 'returns today when launched in the future', () => { diff --git a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts index 8ea911e54ec6..76813df11046 100644 --- a/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts +++ b/projects/packages/premium-analytics/packages/data/src/defaults/reports.ts @@ -3,7 +3,6 @@ */ import { getComparisonRangeFromPreset } from '@jetpack-premium-analytics/datetime'; import { differenceInCalendarDays, startOfDay } from 'date-fns'; - /** * Internal dependencies */ @@ -26,6 +25,7 @@ const DEFAULT_PRESET: PresetType = 'last-30-days'; * - 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 ) { @@ -53,6 +53,8 @@ export function getDefaultPreset( launchedDate?: string ): PresetType { * * 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 = ( /** @@ -73,11 +75,7 @@ export const getDefaultQueryParams = ( const { from: fromString, to: toString } = range; - const interval = getDefaultIntervalForPeriod( - undefined, - fromString, - toString - ); + const interval = getDefaultIntervalForPeriod( undefined, fromString, toString ); if ( ! withComparison ) { return { 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 index 5c5b1649f419..4c76b44e3610 100644 --- 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 @@ -4,7 +4,6 @@ import { useQuery } from '@tanstack/react-query'; import apiFetch from '@wordpress/api-fetch'; import { addQueryArgs } from '@wordpress/url'; - /** * Internal dependencies */ @@ -53,7 +52,7 @@ async function fetchProductImages( path: addQueryArgs( '/wc/v3/products', queryArgs ), } ); - return response.map( ( product ) => ( { + return response.map( product => ( { productId: product.id, imageUrl: product.images?.[ 0 ]?.src || '', imageAlt: product.images?.[ 0 ]?.alt || product.name, @@ -76,10 +75,7 @@ export function useProductImages( params: UseProductImagesParams ) { queryFn: async () => { const images = await fetchProductImages( params.productIds ); return images.reduce( - ( - acc: Record< number, ProductImage >, - image: ProductImage & { productId: number } - ) => { + ( acc: Record< number, ProductImage >, image: ProductImage & { productId: number } ) => { acc[ image.productId ] = { imageUrl: image.imageUrl, imageAlt: image.imageAlt, 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 index fc91af38a3fb..7c50d401510d 100644 --- 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 @@ -5,14 +5,12 @@ 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', - ], + 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 index 9bc2d545d60c..b27ec3f9360f 100644 --- 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 @@ -9,17 +9,17 @@ type UseReportConversionRateOptions = { enabled?: boolean; }; +/** + * + * @param params + * @param options + */ export function useReportConversionRate( params: ReportParams, options?: UseReportConversionRateOptions ) { - return useReport( ( p ) => reportConversionRateQuery( p ), params, { + return useReport( p => reportConversionRateQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'conversion-rate', - '__comparison__', - 'disabled', - ], + 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 index 6ba9daa58d92..a88b7a7b282b 100644 --- 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 @@ -5,13 +5,12 @@ 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', - ], + 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 index 2b2fb11a3617..205e0d4780af 100644 --- 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 @@ -5,13 +5,12 @@ 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', - ], + 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 index 1ddd8c538af0..08217edd8423 100644 --- 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 @@ -9,18 +9,17 @@ type UseReportCustomersByDateOptions = { enabled?: boolean; }; +/** + * + * @param params + * @param options + */ export function useReportCustomersByDate( params: ReportParams, options?: UseReportCustomersByDateOptions ) { - return useReport( ( p ) => reportCustomersByDateQuery( p ), params, { + return useReport( p => reportCustomersByDateQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'customers', - 'by-date', - '__comparison__', - 'disabled', - ], + 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 index 3f4a7a2aee6e..2fa65e6724ec 100644 --- 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 @@ -5,8 +5,12 @@ 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, { + return useReport( p => reportCustomersQuery( p ), params, { disabledComparisonKey: [ 'reports', 'customers', 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 index 7bf18f3e341e..6377f6e68735 100644 --- 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 @@ -16,6 +16,11 @@ const DISABLED_COMPARISON_KEY = [ 'included-in-primary', ]; +/** + * + * @param params + * @param options + */ export function useReportOrderAttribution( params: ReportParams, options?: UseReportOrderAttributionOptions @@ -32,12 +37,7 @@ export function useReportOrderAttribution( // Order attribution requires the view parameter if ( ! params.view ) { return { - queryKey: [ - 'reports', - 'order-attribution', - '__disabled__', - 'no-view-param', - ], + queryKey: [ 'reports', 'order-attribution', '__disabled__', 'no-view-param' ], enabled: false, }; } 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 index 336de035aede..89cf74039adb 100644 --- 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 @@ -9,18 +9,14 @@ type UseReportOrdersOptions = { enabled?: boolean; }; -export function useReportOrders( - params: ReportParams, - options?: UseReportOrdersOptions -) { - return useReport( ( p ) => reportOrdersQuery( p ), params, { +/** + * + * @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', - ], + 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 index bdd30704ffe8..905477389c79 100644 --- 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 @@ -5,13 +5,13 @@ 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', - ], + 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 index 535980817370..7ac25ef8f69b 100644 --- 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 @@ -32,14 +32,8 @@ export function useReportSessionsByDevice( params: ReportParams, options?: UseReportSessionsByDeviceOptions ) { - return useReport( ( p ) => reportSessionsByDeviceQuery( p ), params, { + return useReport( p => reportSessionsByDeviceQuery( p ), params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'sessions', - 'by-device', - '__comparison__', - 'disabled', - ], + 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 index b4252eb16676..ac9b8146508d 100644 --- 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 @@ -12,12 +12,17 @@ type UseReportVisitorsByLocationOptions = { limit?: number; }; +/** + * + * @param params + * @param options + */ export function useReportVisitorsByLocation( params: ReportParams, options?: UseReportVisitorsByLocationOptions ) { return useReport( - ( p ) => + p => reportVisitorsByLocationQuery( { ...p, group_by: options?.groupBy ?? 'country', @@ -27,13 +32,7 @@ export function useReportVisitorsByLocation( params, { enabled: options?.enabled, - disabledComparisonKey: [ - 'reports', - 'visitors', - 'by-location', - '__comparison__', - 'disabled', - ], + 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 index 7fd1805f2744..b6eafd7a3f3f 100644 --- 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 @@ -9,18 +9,14 @@ type UseReportVisitorsOptions = { enabled?: boolean; }; -export function useReportVisitors( - params: ReportParams, - options?: UseReportVisitorsOptions -) { - return useReport( ( p ) => reportVisitorsQuery( p ), params, { +/** + * + * @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', - ], + 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 index 9d90ab78683c..e72a203f2e80 100644 --- a/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts +++ b/projects/packages/premium-analytics/packages/data/src/hooks/use-report.ts @@ -3,7 +3,6 @@ */ import { useQuery, type UseQueryOptions } from '@tanstack/react-query'; import { useCallback } from 'react'; - /** * Internal dependencies */ @@ -81,11 +80,7 @@ export function useReport< TData >( 'comparison' ) : { - queryKey: options?.disabledComparisonKey ?? [ - 'reports', - '__comparison__', - 'disabled', - ], + queryKey: options?.disabledComparisonKey ?? [ 'reports', '__comparison__', 'disabled' ], }; const primary = useQuery( { @@ -95,10 +90,7 @@ export function useReport< TData >( const comparison = useQuery( { ...comparisonQueryOptions, - enabled: - queryEnabled && - comparisonEnabled && - ( comparisonQueryOptions.enabled ?? true ), + enabled: queryEnabled && comparisonEnabled && ( comparisonQueryOptions.enabled ?? true ), } ); // Compute common derived states diff --git a/projects/packages/premium-analytics/packages/data/src/index.ts b/projects/packages/premium-analytics/packages/data/src/index.ts index 0d685aeb5de7..6f02580c031e 100644 --- a/projects/packages/premium-analytics/packages/data/src/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/index.ts @@ -1,15 +1,6 @@ -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 { 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'; @@ -44,10 +35,7 @@ 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 { 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/prefetch-report.ts b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts index b5f494b174f7..b73aac40dca0 100644 --- a/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts +++ b/projects/packages/premium-analytics/packages/data/src/prefetch/prefetch-report.ts @@ -18,22 +18,23 @@ import { type RequestReportParamsMap = { orders: Parameters< typeof reportOrdersQuery >[ 0 ]; - 'order-attribution': Parameters< - typeof reportOrderAttributionSummaryQuery - >[ 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 ]; + '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 ] @@ -41,9 +42,7 @@ export async function prefetchReport< T extends keyof RequestReportParamsMap >( switch ( reportType ) { case 'orders': return queryClient.ensureQueryData( - reportOrdersQuery( - params as RequestReportParamsMap[ 'orders' ] - ) + reportOrdersQuery( params as RequestReportParamsMap[ 'orders' ] ) ); case 'order-attribution': @@ -55,65 +54,47 @@ export async function prefetchReport< T extends keyof RequestReportParamsMap >( case 'coupons': return queryClient.ensureQueryData( - reportCouponsQuery( - params as RequestReportParamsMap[ 'coupons' ] - ) + reportCouponsQuery( params as RequestReportParamsMap[ 'coupons' ] ) ); case 'coupons-by-date': return queryClient.ensureQueryData( - reportCouponsByDateQuery( - params as RequestReportParamsMap[ 'coupons-by-date' ] - ) + reportCouponsByDateQuery( params as RequestReportParamsMap[ 'coupons-by-date' ] ) ); case 'customers': return queryClient.ensureQueryData( - reportCustomersQuery( - params as RequestReportParamsMap[ 'customers' ] - ) + reportCustomersQuery( params as RequestReportParamsMap[ 'customers' ] ) ); case 'customers-by-date': return queryClient.ensureQueryData( - reportCustomersByDateQuery( - params as RequestReportParamsMap[ 'customers-by-date' ] - ) + reportCustomersByDateQuery( params as RequestReportParamsMap[ 'customers-by-date' ] ) ); case 'visitors': return queryClient.ensureQueryData( - reportVisitorsQuery( - params as RequestReportParamsMap[ 'visitors' ] - ) + reportVisitorsQuery( params as RequestReportParamsMap[ 'visitors' ] ) ); case 'visitors-by-location': return queryClient.ensureQueryData( - reportVisitorsByLocationQuery( - params as RequestReportParamsMap[ 'visitors-by-location' ] - ) + reportVisitorsByLocationQuery( params as RequestReportParamsMap[ 'visitors-by-location' ] ) ); case 'sessions-by-device': return queryClient.ensureQueryData( - reportSessionsByDeviceQuery( - params as RequestReportParamsMap[ 'sessions-by-device' ] - ) + reportSessionsByDeviceQuery( params as RequestReportParamsMap[ 'sessions-by-device' ] ) ); case 'products': return queryClient.ensureQueryData( - reportProductsQuery( - params as RequestReportParamsMap[ 'products' ] - ) + reportProductsQuery( params as RequestReportParamsMap[ 'products' ] ) ); case 'conversion-rate': return queryClient.ensureQueryData( - reportConversionRateQuery( - params as RequestReportParamsMap[ 'conversion-rate' ] - ) + reportConversionRateQuery( params as RequestReportParamsMap[ 'conversion-rate' ] ) ); default: 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 index 0d656d471de8..7073cb9e7d32 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/bookings/index.ts @@ -2,14 +2,11 @@ * Internal dependencies */ import { fetchReportBookings } from '../../api/report-bookings-fetch'; -import type { Override } from '../../utils/types'; import { safeParseInt } from '../../utils/parsing'; +import type { Override } from '../../utils/types'; -type ReportsBookingsByDateResponse = Awaited< - ReturnType< typeof fetchReportBookings > ->; -type RawBookingsReportDataItem = - ReportsBookingsByDateResponse[ 'data' ][ number ]; +type ReportsBookingsByDateResponse = Awaited< ReturnType< typeof fetchReportBookings > >; +type RawBookingsReportDataItem = ReportsBookingsByDateResponse[ 'data' ][ number ]; type RawBookingsReportSummaryItem = ReportsBookingsByDateResponse[ 'summary' ]; type SanitizedBookingsByDateItem = Override< @@ -44,32 +41,26 @@ type SanitizedBookingsSummaryItem = Override< /** * Sanitize/process a single booking item by converting strings to numbers + * @param item */ -function sanitizeBookingItem( - item: RawBookingsReportDataItem -): SanitizedBookingsByDateItem { +function sanitizeBookingItem( item: RawBookingsReportDataItem ): SanitizedBookingsByDateItem { return { ...item, status_unpaid: safeParseInt( item.status_unpaid ), - status_pending_confirmation: safeParseInt( - item.status_pending_confirmation - ), + 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 - ), + 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 @@ -77,20 +68,14 @@ function sanitizeBookingSummaryItem( return { ...item, status_unpaid: safeParseInt( item.status_unpaid ), - status_pending_confirmation: safeParseInt( - item.status_pending_confirmation - ), + 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 - ), + attendance_status_no_show: safeParseInt( item.attendance_status_no_show ), + attendance_status_checked_in: safeParseInt( item.attendance_status_checked_in ), }; } @@ -108,6 +93,7 @@ type SanitizedBookingsByDateResponse = { * * 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 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 index 3e33c27c69af..1135cf3a43f0 100644 --- 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 @@ -2,19 +2,17 @@ * External dependencies */ import { __ } from '@wordpress/i18n'; - /** * Internal dependencies */ import { fetchReportConversionRate } from '../../api/report-conversion-rate-fetch'; -import type { Override } from '../../utils/types'; import { safeParseInt } from '../../utils/parsing'; +import type { Override } from '../../utils/types'; type ReportsConversionRateByDateResponse = Awaited< ReturnType< typeof fetchReportConversionRate > >; -type RawConversionRateReportDataItem = - ReportsConversionRateByDateResponse[ 'data' ][ number ]; +type RawConversionRateReportDataItem = ReportsConversionRateByDateResponse[ 'data' ][ number ]; type SanitizedConversionRateByDateItem = Override< RawConversionRateReportDataItem, { @@ -30,6 +28,7 @@ type SanitizedConversionRateByDateItem = Override< /** * Sanitize/process a single conversion rate item by converting strings to numbers * and calculating the conversion rate + * @param item */ function sanitizeConversionRateItem( item: RawConversionRateReportDataItem @@ -42,8 +41,7 @@ function sanitizeConversionRateItem( // 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; + const conversionRate = activeSessionsNum > 0 ? completedCheckoutNum / activeSessionsNum : 0; return { ...item, @@ -82,6 +80,7 @@ type SanitizedConversionRateByDateResponse = { * * 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 @@ -97,9 +96,7 @@ export const sanitizeReportConversionRateResponse = ( date_end: '', }; - const sanitizedSummary = sanitizeConversionRateItem( - response?.summary || defaultSummary - ); + const sanitizedSummary = sanitizeConversionRateItem( response?.summary || defaultSummary ); // Create funnel steps from the summary data const steps: FunnelStep[] = [ @@ -115,9 +112,7 @@ export const sanitizeReportConversionRateResponse = ( count: sanitizedSummary.with_cart_addition, rate: sanitizedSummary.active_sessions > 0 - ? ( sanitizedSummary.with_cart_addition / - sanitizedSummary.active_sessions ) * - 100 + ? ( sanitizedSummary.with_cart_addition / sanitizedSummary.active_sessions ) * 100 : 0, }, { @@ -126,9 +121,7 @@ export const sanitizeReportConversionRateResponse = ( count: sanitizedSummary.reached_checkout, rate: sanitizedSummary.active_sessions > 0 - ? ( sanitizedSummary.reached_checkout / - sanitizedSummary.active_sessions ) * - 100 + ? ( sanitizedSummary.reached_checkout / sanitizedSummary.active_sessions ) * 100 : 0, }, { @@ -137,18 +130,14 @@ export const sanitizeReportConversionRateResponse = ( count: sanitizedSummary.completed_checkout, rate: sanitizedSummary.active_sessions > 0 - ? ( sanitizedSummary.completed_checkout / - sanitizedSummary.active_sessions ) * - 100 + ? ( sanitizedSummary.completed_checkout / sanitizedSummary.active_sessions ) * 100 : 0, }, ]; return { summary: sanitizedSummary, - data: response?.data - ? response.data.map( sanitizeConversionRateItem ) - : [], + 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 index 53b5720d37a0..c8af73181fce 100644 --- 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 @@ -4,9 +4,7 @@ import { fetchReportCouponsByDate } from '../../api/report-coupons-by-date-fetch'; import type { Override } from '../../utils/types'; -type ReportsCouponsByDateResponse = Awaited< - ReturnType< typeof fetchReportCouponsByDate > ->; +type ReportsCouponsByDateResponse = Awaited< ReturnType< typeof fetchReportCouponsByDate > >; type RawSummary = ReportsCouponsByDateResponse[ 'summary' ]; type RawDataItem = ReportsCouponsByDateResponse[ 'data' ][ number ]; @@ -54,6 +52,10 @@ type SanitizedCouponsByDateResponse = { data: SanitizedCouponsByDateDataItem[]; }; +/** + * + * @param item + */ function sanitizeItem( item: RawDataItem ): SanitizedCouponsByDateDataItem { return { ...item, @@ -69,6 +71,10 @@ function sanitizeItem( item: RawDataItem ): SanitizedCouponsByDateDataItem { }; } +/** + * + * @param summary + */ function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { return { ...summary, @@ -79,9 +85,7 @@ function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { 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 - ), + net_sales_after_discount: parseFloat( summary.net_sales_after_discount ), coupon_usage_percentage: parseFloat( summary.coupon_usage_percentage ), }; } @@ -89,6 +93,7 @@ function sanitizeSummary( summary: RawSummary ): SanitizedCouponsByDateSummary { /** * 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 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 index 04c76205f2c5..1f40db489cd3 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/coupons/index.ts @@ -4,9 +4,7 @@ import { fetchReportCoupons } from '../../api/report-coupons-fetch'; import type { Override } from '../../utils/types'; -type ReportsCouponsResponse = Awaited< - ReturnType< typeof fetchReportCoupons > ->; +type ReportsCouponsResponse = Awaited< ReturnType< typeof fetchReportCoupons > >; type RawCouponsDataItem = ReportsCouponsResponse[ 'data' ][ number ]; type RawCouponsDataSummary = ReportsCouponsResponse[ 'summary' ]; @@ -44,10 +42,9 @@ type SanitizedCouponsResponse = { /** * Sanitize/process a single coupon item by converting strings to numbers + * @param item */ -function sanitizeCouponItem( - item: RawCouponsDataItem -): SanitizedCouponsDataItem { +function sanitizeCouponItem( item: RawCouponsDataItem ): SanitizedCouponsDataItem { return { ...item, discount_amount: parseFloat( item.discount_amount ), @@ -58,10 +55,9 @@ function sanitizeCouponItem( /** * Sanitize/process summary by converting strings to numbers + * @param summary */ -function sanitizeCouponSummary( - summary: RawCouponsDataSummary -): SanitizedCouponsDataSummary { +function sanitizeCouponSummary( summary: RawCouponsDataSummary ): SanitizedCouponsDataSummary { return { ...summary, total_sales: parseFloat( summary.total_sales ), @@ -73,6 +69,7 @@ function sanitizeCouponSummary( /** * 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 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 index 1c61302ae3c5..8b098d7adde2 100644 --- 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 @@ -4,12 +4,9 @@ import { fetchReportCustomersByDate } from '../../api/report-customers-by-date-fetch'; import type { Override } from '../../utils/types'; -type ReportsCustomersByDateResponse = Awaited< - ReturnType< typeof fetchReportCustomersByDate > ->; +type ReportsCustomersByDateResponse = Awaited< ReturnType< typeof fetchReportCustomersByDate > >; type RawCustomersByDateSummary = ReportsCustomersByDateResponse[ 'summary' ]; -type RawCustomersByDateItem = - ReportsCustomersByDateResponse[ 'data' ][ number ]; +type RawCustomersByDateItem = ReportsCustomersByDateResponse[ 'data' ][ number ]; /** * Processed summary (numbers for calculations) @@ -76,10 +73,9 @@ export type SanitizedCustomersByDateResponse = { /** * Sanitize/process a single customer item by converting strings to numbers + * @param item */ -function sanitizeCustomerByDateItem( - item: RawCustomersByDateItem -): SanitizedCustomersByDateItem { +function sanitizeCustomerByDateItem( item: RawCustomersByDateItem ): SanitizedCustomersByDateItem { const totalCustomers = parseInt( item.total_customers, 10 ); return { ...item, @@ -88,15 +84,10 @@ function sanitizeCustomerByDateItem( 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 - ), + 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 - ), + returning_customer_net_sales: parseFloat( item.returning_customer_net_sales ), // Add alias for compatibility with chart builder customers: totalCustomers, }; @@ -104,6 +95,7 @@ function sanitizeCustomerByDateItem( /** * Sanitize/process the summary by converting strings to numbers + * @param summary */ function sanitizeCustomerByDateSummary( summary: RawCustomersByDateSummary @@ -116,47 +108,24 @@ function sanitizeCustomerByDateSummary( 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_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_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 - ), + 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 ), @@ -168,6 +137,7 @@ function sanitizeCustomerByDateSummary( /** * 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 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 index be0fffcf7844..780eca7c831f 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/customers/index.ts @@ -4,13 +4,9 @@ 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 ]; +type ReportsCustomersNewReturningResponse = Awaited< ReturnType< typeof fetchReportCustomers > >; +type RawCustomersNewReturningSummary = ReportsCustomersNewReturningResponse[ 'summary' ]; +type RawCustomersNewReturningItem = ReportsCustomersNewReturningResponse[ 'data' ][ number ]; /** * Processed summary (numbers for calculations) @@ -46,6 +42,7 @@ type SanitizedCustomersNewReturningResponse = { /** * Sanitize/process a single customer item by converting strings to numbers + * @param item */ function sanitizeCustomerItem( item: RawCustomersNewReturningItem @@ -59,6 +56,7 @@ function sanitizeCustomerItem( /** * Sanitize/process the summary by converting strings to numbers + * @param summary */ function sanitizeCustomerSummary( summary: RawCustomersNewReturningSummary @@ -68,15 +66,14 @@ function sanitizeCustomerSummary( 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 - ), + 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 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 index 129d281b23a4..5ed83bf8404e 100644 --- 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 @@ -20,25 +20,21 @@ export function normalizeOrderAttributionByProductResponse( previousResponse?: OrderAttributionByProductResponse ): OrderAttributionSummaryResponse { // Create a map for quick lookup of previous period data by item - const previousDataMap = new Map< - string, - ( typeof currentResponse.data )[ 0 ] - >(); + const previousDataMap = new Map< string, ( typeof currentResponse.data )[ 0 ] >(); if ( previousResponse ) { - previousResponse.data.forEach( ( item ) => { + previousResponse.data.forEach( item => { previousDataMap.set( item.item, item ); } ); } // Transform the flat structure to nested structure - const normalizedData = currentResponse.data.map( ( currentItem ) => { + 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; + const previousIntervals = previousItem?.intervals || currentItem.intervals; return { item: currentItem.item, @@ -56,22 +52,18 @@ export function normalizeOrderAttributionByProductResponse( // 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 - ); + 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', - } ) - ), + intervals: previousItem.intervals.map( interval => ( { + ...interval, + net_sales: '0', + } ) ), }, previous_period: { value: previousItem.value, 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 index 7634d328fae4..09572282b098 100644 --- 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 @@ -1,8 +1,8 @@ /** * Internal dependencies */ -import { sanitizeStringNumber } from '../utils'; import { fetchReportOrderAttributionSummary } from '../../api/report-order-attribution-summary-fetch'; +import { sanitizeStringNumber } from '../utils'; type OrderAttributionSummaryResponse = Awaited< ReturnType< typeof fetchReportOrderAttributionSummary > @@ -60,6 +60,7 @@ export type SanitizedOrderAttributionSummaryResponse = { /** * Sanitizes a single interval by converting string net_sales to number + * @param interval */ function sanitizeOrderAttributionInterval( interval: OrderAttributionInterval @@ -74,6 +75,7 @@ function sanitizeOrderAttributionInterval( /** * Sanitizes a period by converting value to number and intervals + * @param period */ function sanitizeOrderAttributionPeriod( period: OrderAttributionPeriod @@ -86,6 +88,7 @@ function sanitizeOrderAttributionPeriod( /** * Sanitizes a single order attribution summary item + * @param item */ function sanitizeOrderAttributionSummaryItem( item: OrderAttributionSummaryItem 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 index faf1f71af07e..75be705212bb 100644 --- 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 @@ -1,12 +1,12 @@ /** * Internal dependencies */ +import { safeParseFloat, safeParseInt } from '../../utils/parsing'; import type { ReportsOrdersByDateResponse, RequestReportOrdersParams, } from '../../api/report-orders-fetch'; import type { Override } from '../../utils/types'; -import { safeParseFloat, safeParseInt } from '../../utils/parsing'; /** * Re-export the request params type for backwards compatibility. @@ -37,6 +37,7 @@ type SanitizedOrdersByProductTypeByDateItem = Override< /** * Sanitize/process a single orders by product type item by converting strings to numbers + * @param item */ function sanitizeOrdersByProductTypeItem( item: RawOrdersByProductTypeReportDataItem @@ -71,6 +72,7 @@ type SanitizedOrdersByProductTypeByDateResponse = { * * 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 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 index 32dc9661797b..00b7a8acf6d4 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/orders/index.ts @@ -2,12 +2,10 @@ * Internal dependencies */ import { fetchReportOrders } from '../../api/report-orders-fetch'; -import type { Override } from '../../utils/types'; import { safeParseFloat, safeParseInt } from '../../utils/parsing'; +import type { Override } from '../../utils/types'; -type ReportsOrdersByDateResponse = Awaited< - ReturnType< typeof fetchReportOrders > ->; +type ReportsOrdersByDateResponse = Awaited< ReturnType< typeof fetchReportOrders > >; type RawOrdersReportDataItem = ReportsOrdersByDateResponse[ 'data' ][ number ]; type SanitizedOrdersByDateItem = Override< RawOrdersReportDataItem, @@ -32,10 +30,9 @@ type SanitizedOrdersByDateItem = Override< /** * Sanitize/process a single order item by converting strings to numbers + * @param item */ -function sanitizeOrderItem( - item: RawOrdersReportDataItem -): SanitizedOrdersByDateItem { +function sanitizeOrderItem( item: RawOrdersReportDataItem ): SanitizedOrdersByDateItem { return { ...item, average_order_value: safeParseFloat( item.average_order_value ), @@ -70,6 +67,7 @@ type SanitizedOrdersByDateResponse = { * * 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 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 index a3937da22a13..6d2ba237964f 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/products/index.ts @@ -4,9 +4,7 @@ import { fetchReportProducts } from '../../api/report-products-fetch'; import type { Override } from '../../utils/types'; -type ReportProductsResponse = Awaited< - ReturnType< typeof fetchReportProducts > ->; +type ReportProductsResponse = Awaited< ReturnType< typeof fetchReportProducts > >; type RawProductsReportDataItem = ReportProductsResponse[ 'data' ][ number ]; type RawProductsReportSummary = ReportProductsResponse[ 'summary' ]; @@ -33,10 +31,9 @@ type SanitizedProductsSummary = Override< /** * Sanitize/process a single product item by converting strings to numbers + * @param item */ -function sanitizeProductItem( - item: RawProductsReportDataItem -): SanitizedProductsItem { +function sanitizeProductItem( item: RawProductsReportDataItem ): SanitizedProductsItem { return { ...item, product_id: parseInt( item.product_id, 10 ), @@ -46,9 +43,11 @@ function sanitizeProductItem( }; } -function sanitizeProductSummary( - summary: RawProductsReportSummary -): SanitizedProductsSummary { +/** + * + * @param summary + */ +function sanitizeProductSummary( summary: RawProductsReportSummary ): SanitizedProductsSummary { return { ...summary, total_orders: parseInt( summary.total_orders, 10 ), @@ -72,6 +71,7 @@ type SanitizedProductsResponse = { * * 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 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 index f88fc25fc0c4..127c39843e31 100644 --- 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 @@ -6,15 +6,12 @@ import { fetchReportSessionsByDevice } from '../../api/report-sessions-by-device /** * Inferred types from fetch response */ -type ReportsSessionsByDeviceResponse = Awaited< - ReturnType< typeof fetchReportSessionsByDevice > ->; +type ReportsSessionsByDeviceResponse = Awaited< ReturnType< typeof fetchReportSessionsByDevice > >; /** * Raw item type from API response */ -type SessionsByDeviceItem = - ReportsSessionsByDeviceResponse[ 'data' ][ number ]; +type SessionsByDeviceItem = ReportsSessionsByDeviceResponse[ 'data' ][ number ]; /** * Sanitized item with numeric values @@ -44,9 +41,7 @@ type SanitizedSessionsByDeviceResponse = { * * @param item - Raw item from API response */ -function sanitizeSessionsByDeviceItem( - item: SessionsByDeviceItem -): SanitizedSessionsByDeviceItem { +function sanitizeSessionsByDeviceItem( item: SessionsByDeviceItem ): SanitizedSessionsByDeviceItem { return { device_type: item.device_type || '', active_sessions: parseInt( item.active_sessions, 10 ) || 0, @@ -66,13 +61,10 @@ export const sanitizeReportSessionsByDeviceResponse = ( ): SanitizedSessionsByDeviceResponse => { const items = response?.data ?? []; const data = items - .filter( ( item ) => item.device_type ) // Filter out empty device types + .filter( item => item.device_type ) // Filter out empty device types .map( sanitizeSessionsByDeviceItem ); - const totalSessions = data.reduce( - ( acc, item ) => acc + item.active_sessions, - 0 - ); + const totalSessions = data.reduce( ( acc, item ) => acc + item.active_sessions, 0 ); return { summary: { 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 index 509856885bc9..b9dac4936c16 100644 --- 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 @@ -10,11 +10,8 @@ import type { Override } from '../../utils/types'; type ReportsVisitorsByLocationResponse = Awaited< ReturnType< typeof fetchReportVisitorsByLocation > >; -type RawVisitorsByLocationItem = - ReportsVisitorsByLocationResponse[ 'data' ][ number ]; -type RawVisitorsByLocationSummary = NonNullable< - ReportsVisitorsByLocationResponse[ 'summary' ] ->; +type RawVisitorsByLocationItem = ReportsVisitorsByLocationResponse[ 'data' ][ number ]; +type RawVisitorsByLocationSummary = NonNullable< ReportsVisitorsByLocationResponse[ 'summary' ] >; type SanitizedVisitorsByLocationItem = Override< RawVisitorsByLocationItem, @@ -30,6 +27,10 @@ type SanitizedVisitorsByLocationSummary = Override< } >; +/** + * + * @param item + */ function sanitizeVisitorsByLocationItem( item: RawVisitorsByLocationItem ): SanitizedVisitorsByLocationItem { @@ -41,6 +42,10 @@ function sanitizeVisitorsByLocationItem( }; } +/** + * + * @param summary + */ function sanitizeVisitorsByLocationSummary( summary: RawVisitorsByLocationSummary ): SanitizedVisitorsByLocationSummary { @@ -67,11 +72,7 @@ export const sanitizeReportVisitorsByLocationResponse = ( }; return { - summary: sanitizeVisitorsByLocationSummary( - response?.summary ?? defaultSummary - ), - data: response?.data - ? response.data.map( sanitizeVisitorsByLocationItem ) - : [], + 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 index 3a787c37cba6..7d14cf71ebc1 100644 --- a/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/processing/visitors/index.ts @@ -7,11 +7,8 @@ import type { Override } from '../../utils/types'; /** * Inferred types */ -type ReportsVisitorsByDateResponse = Awaited< - ReturnType< typeof fetchReportVisitors > ->; -type RawVisitorsReportDataItem = - ReportsVisitorsByDateResponse[ 'data' ][ number ]; +type ReportsVisitorsByDateResponse = Awaited< ReturnType< typeof fetchReportVisitors > >; +type RawVisitorsReportDataItem = ReportsVisitorsByDateResponse[ 'data' ][ number ]; type RawVisitorsReportDataSummary = ReportsVisitorsByDateResponse[ 'summary' ]; type SanitizedVisitorsByDateItem = Override< @@ -40,10 +37,9 @@ type SanitizeVisitorsItemArg = Override< /** * Sanitize/process a single visitors item by converting strings to numbers + * @param item */ -function sanitizeVisitorsItem( - item: SanitizeVisitorsItemArg -): SanitizedVisitorsByDateItem { +function sanitizeVisitorsItem( item: SanitizeVisitorsItemArg ): SanitizedVisitorsByDateItem { return { ...item, active_sessions: parseInt( item.active_sessions, 10 ), @@ -65,6 +61,7 @@ type SanitizedVisitorsByDateResponse = { * * 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 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 index 1a03b0e0dd64..cf1058f6255e 100644 --- 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 @@ -1,6 +1,7 @@ /** * External dependencies */ +import { onlineManager } from '@tanstack/react-query'; import { createContext, useContext, @@ -9,15 +10,10 @@ import { useSyncExternalStore, type ReactNode, } from 'react'; -import { onlineManager } from '@tanstack/react-query'; - /** * Internal dependencies */ -import { - globalErrorManager, - type GlobalErrorType, -} from './global-error-manager'; +import { globalErrorManager, type GlobalErrorType } from './global-error-manager'; interface GlobalErrorContextValue { globalError: GlobalErrorType; @@ -26,13 +22,13 @@ interface GlobalErrorContextValue { isGlobalError: boolean; } -const GlobalErrorContext = createContext< GlobalErrorContextValue | null >( - null -); +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( @@ -54,7 +50,7 @@ export function GlobalErrorProvider( { children }: { children: ReactNode } ) { globalErrorManager.setError( 'network' ); } - const unsubscribe = onlineManager.subscribe( ( isOnline ) => { + const unsubscribe = onlineManager.subscribe( isOnline => { if ( ! isOnline ) { globalErrorManager.setError( 'network' ); } else if ( globalErrorManager.getError() === 'network' ) { @@ -76,9 +72,7 @@ export function GlobalErrorProvider( { children }: { children: ReactNode } ) { ); return ( - - { children } - + { children } ); } 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 index 5cc52b9b003a..9686d09e595b 100644 --- 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 @@ -21,7 +21,7 @@ class GlobalErrorManager { return; } this.error = error; - this.listeners.forEach( ( listener ) => listener() ); + this.listeners.forEach( listener => listener() ); }; clearError = (): void => this.setError( null ); diff --git a/projects/packages/premium-analytics/packages/data/src/providers/index.ts b/projects/packages/premium-analytics/packages/data/src/providers/index.ts index 641d073e3b48..9b6bc769e9bf 100644 --- a/projects/packages/premium-analytics/packages/data/src/providers/index.ts +++ b/projects/packages/premium-analytics/packages/data/src/providers/index.ts @@ -1,11 +1,5 @@ -export { - queryClient, - AnalyticsQueryClientProvider, -} from './query-client-provider'; +export { queryClient, AnalyticsQueryClientProvider } from './query-client-provider'; export { GlobalErrorProvider, useGlobalError } from './global-error-context'; -export { - globalErrorManager, - type GlobalErrorType, -} from './global-error-manager'; +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 index c6ddf592efbc..1baf4f0ea5be 100644 --- 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 @@ -1,13 +1,8 @@ /** * External dependencies */ -import { - QueryClient, - QueryClientProvider, - QueryCache, -} from '@tanstack/react-query'; +import { QueryClient, QueryClientProvider, QueryCache } from '@tanstack/react-query'; import { ReactNode, lazy, Suspense } from 'react'; - /** * Internal dependencies */ @@ -39,8 +34,7 @@ function areQueryDevtoolsEnabled(): boolean { } const ReactQueryDevtoolsProduction = lazy( () => - // eslint-disable-next-line import/no-extraneous-dependencies -- DevTools is intentionally in devDependencies, only loaded when enabled - import( '@tanstack/react-query-devtools/production' ).then( ( d ) => ( { + import( '@tanstack/react-query-devtools/production' ).then( d => ( { default: d.ReactQueryDevtools, } ) ) ); @@ -48,6 +42,7 @@ const ReactQueryDevtoolsProduction = lazy( () => /** * 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' ) { @@ -97,7 +92,7 @@ function getErrorStatus( error: unknown ): number | null { * Network errors are handled separately in GlobalErrorProvider via onlineManager. */ const queryCache = new QueryCache( { - onError: ( error ) => { + onError: error => { const currentError = globalErrorManager.getError(); // Don't override network error (highest priority) @@ -153,11 +148,7 @@ export const queryClient = new QueryClient( { }, } ); -export const AnalyticsQueryClientProvider = ( { - children, -}: { - children: ReactNode; -} ) => { +export const AnalyticsQueryClientProvider = ( { children }: { children: ReactNode } ) => { return ( <>{ children } 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 index f1a4cdf921c4..8a223b3f8ce9 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,23 +8,17 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 ]; +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; + [ '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' ] > { @@ -43,7 +36,8 @@ export function reportBookingsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index 5133245335d3..7e60af58f333 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,25 +8,18 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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; +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 > -> { +): UseQueryOptions< ReturnType< typeof sanitizeReportConversionRateResponse > > { return { queryKey: getReportConversionRateQueryKey( params ), queryFn: async () => { @@ -42,7 +34,8 @@ export function reportConversionRateQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index fe9e4d09b18e..a02418ab87e1 100644 --- 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 @@ -1,33 +1,27 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ import { fetchReportCouponsByDate } from '../api'; import { sanitizeReportCouponsByDateResponse } from '../processing/coupons-by-date'; -import type { ReportDataMap } from '../types'; import { FilterCondition } from '../types/filter-condition'; +import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportCouponsByDateParams = Parameters< - typeof fetchReportCouponsByDate ->[ 0 ] & { +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; + [ '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' ] > { @@ -45,7 +39,8 @@ export function reportCouponsByDateQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index 05244b7017ba..c37f0d78ec0d 100644 --- 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 @@ -1,33 +1,27 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ import { fetchReportCoupons } from '../api'; import { sanitizeReportCouponsResponse } from '../processing/coupons'; -import type { ReportDataMap } from '../types'; import { FilterCondition } from '../types/filter-condition'; +import type { ReportDataMap } from '../types'; +import type { UseQueryOptions } from '@tanstack/react-query'; -type RequestReportCouponsParams = Parameters< - typeof fetchReportCoupons ->[ 0 ] & { +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; + [ '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' ] > { @@ -45,7 +39,8 @@ export function reportCouponsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index b030320e91e4..1537d84c7353 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,24 +8,17 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 ]; +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; +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' ] > { @@ -44,7 +36,8 @@ export function reportCustomersByDateQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index c214fd28c6ed..0cfdb1a30cfa 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,10 +8,9 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 ]; +type RequestReportCustomersParams = Parameters< typeof fetchReportCustomers >[ 0 ]; const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [ 'reports', @@ -24,6 +22,10 @@ const getReportCustomersQueryKey = ( p: RequestReportCustomersParams ) => [ p.filters, ]; +/** + * + * @param params + */ export function reportCustomersQuery( params: RequestReportCustomersParams ): UseQueryOptions< ReportDataMap[ 'customers' ] > { @@ -42,7 +44,8 @@ export function reportCustomersQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index deda11889eaf..55be6f0e756d 100644 --- 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 @@ -1,22 +1,19 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies */ -import { - fetchReportOrderAttributionSummary, - fetchReportOrderAttributionByProduct, -} from '../api'; +import { fetchReportOrderAttributionSummary, fetchReportOrderAttributionByProduct } from '../api'; import { sanitizeReportOrderAttributionSummaryResponse, normalizeOrderAttributionByProductResponse, type SanitizedOrderAttributionSummaryResponse, } from '../processing/order-attribution'; -import type { FilterCondition } from '../types/filter-condition'; 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 @@ -29,10 +26,9 @@ type ReportOrderAttributionSummaryParams = Parameters< * * 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 -) => +const getReportOrderAttributionQueryKey = ( params: ReportOrderAttributionSummaryParams ) => [ 'reports', 'order-attribution', @@ -80,43 +76,37 @@ export function reportOrderAttributionSummaryQuery( const shouldFetchComparison = compare_from && compare_to && - ( compare_from !== params.from || - compare_to !== params.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 ), - ] - ); + 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 + const normalizedResponse = normalizeOrderAttributionByProductResponse( + currentResponse, + previousResponse ); + + return sanitizeReportOrderAttributionSummaryResponse( normalizedResponse ); } // Regular API path: Returns both primary and comparison in one response @@ -128,17 +118,13 @@ export function reportOrderAttributionSummaryQuery( * 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 - ), + 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, + 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 index 50703f37728c..29deb2e9a552 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,6 +8,7 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 ]; @@ -22,6 +22,10 @@ const getReportOrdersQueryKey = ( p: RequestReportOrdersParams ) => [ p.filters || [], ]; +/** + * + * @param params + */ export function reportOrdersQuery( params: RequestReportOrdersParams ): UseQueryOptions< ReportDataMap[ 'orders' ] > { @@ -39,7 +43,8 @@ export function reportOrdersQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index 4696117be2e2..a46bb686d4ed 100644 --- 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 @@ -1,21 +1,17 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * 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 RequestReportProductsParams = Parameters< typeof fetchReportProducts >[ 0 ]; -type SanitizedProductsResponse = ReturnType< - typeof sanitizeReportProductsResponse ->; +type SanitizedProductsResponse = ReturnType< typeof sanitizeReportProductsResponse >; const getReportProductsQueryKey = ( p: RequestReportProductsParams ) => [ @@ -30,6 +26,10 @@ const getReportProductsQueryKey = ( p: RequestReportProductsParams ) => p.filters, ] as const; +/** + * + * @param params + */ export function reportProductsQuery( params: RequestReportProductsParams ): UseQueryOptions< SanitizedProductsResponse > { @@ -47,7 +47,8 @@ export function reportProductsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index 8e425b7e7127..b00ce00bf33c 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,14 +8,12 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 ]; +type RequestReportSessionsByDeviceParams = Parameters< typeof fetchReportSessionsByDevice >[ 0 ]; -const getReportSessionsByDeviceQueryKey = ( - p: RequestReportSessionsByDeviceParams -) => [ 'reports', 'sessions', 'by-device', p.from, p.to ] as const; +const getReportSessionsByDeviceQueryKey = ( p: RequestReportSessionsByDeviceParams ) => + [ 'reports', 'sessions', 'by-device', p.from, p.to ] as const; /** * Creates query options for fetching sessions by device report data. @@ -41,7 +38,8 @@ export function reportSessionsByDeviceQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index a75988200065..32663a38b75e 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,14 +8,13 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 -) => +const getReportVisitorsByLocationQueryKey = ( p: RequestReportVisitorsByLocationParams ) => [ 'reports', 'visitors', @@ -29,6 +27,10 @@ const getReportVisitorsByLocationQueryKey = ( p.limit ?? null, ] as const; +/** + * + * @param params + */ export function reportVisitorsByLocationQuery( params: RequestReportVisitorsByLocationParams ): UseQueryOptions< ReportDataMap[ 'visitorsByLocation' ] > { @@ -41,6 +43,6 @@ export function reportVisitorsByLocationQuery( enabled: !! ( params.from && params.to && params.interval ), - placeholderData: ( previousData ) => previousData, + 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 index d3749c184256..78d2f138d554 100644 --- 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 @@ -1,7 +1,6 @@ /** * External dependencies */ -import type { UseQueryOptions } from '@tanstack/react-query'; /** * Internal dependencies @@ -9,22 +8,17 @@ import type { UseQueryOptions } from '@tanstack/react-query'; 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 ]; +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; + [ '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' ] > { @@ -42,7 +36,8 @@ export function reportVisitorsQuery( /** * Keep previous data while fetching new data to prevent blank states + * @param previousData */ - placeholderData: ( previousData ) => 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 index 8ad61e0cbbd0..76b5d9f1a945 100644 --- a/projects/packages/premium-analytics/packages/data/src/types.ts +++ b/projects/packages/premium-analytics/packages/data/src/types.ts @@ -1,21 +1,18 @@ /** * Internal dependencies */ -import { - sanitizeReportOrdersResponse, - sanitizeReportProductsResponse, -} from './processing'; +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 { sanitizeReportCouponsResponse } from './processing/coupons'; -import { sanitizeReportCouponsByDateResponse } from './processing/coupons-by-date'; -import { sanitizeReportVisitorsResponse } from './processing/visitors'; -import { sanitizeReportVisitorsByLocationResponse } from './processing/visitors-by-location'; -import { sanitizeReportConversionRateResponse } from './processing/conversion-rate'; import { sanitizeReportOrdersByProductTypeResponse } from './processing/orders-by-product-type'; -import { sanitizeReportBookingsResponse } from './processing/bookings'; 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 = @@ -38,9 +35,7 @@ export type QueryParams = ReportParams & { }; // Inferred from processing/orders.ts -type SanitizedOrdersByDateResponse = ReturnType< - typeof sanitizeReportOrdersResponse ->; +type SanitizedOrdersByDateResponse = ReturnType< typeof sanitizeReportOrdersResponse >; // Inferred from processing/order-attribution.ts type SanitizedOrderAttributionSummaryResponse = ReturnType< @@ -48,34 +43,22 @@ type SanitizedOrderAttributionSummaryResponse = ReturnType< >; // Inferred from processing/coupons.ts -type SanitizedCouponsResponse = ReturnType< - typeof sanitizeReportCouponsResponse ->; +type SanitizedCouponsResponse = ReturnType< typeof sanitizeReportCouponsResponse >; // Inferred from processing/coupons-by-date/index.ts -type SanitizedCouponsByDateResponse = ReturnType< - typeof sanitizeReportCouponsByDateResponse ->; +type SanitizedCouponsByDateResponse = ReturnType< typeof sanitizeReportCouponsByDateResponse >; // Inferred from processing/customers.ts -type SanitizedCustomersResponse = ReturnType< - typeof sanitizeReportCustomersResponse ->; +type SanitizedCustomersResponse = ReturnType< typeof sanitizeReportCustomersResponse >; // Inferred from processing/customers-by-date/index.ts -type SanitizedCustomersByDateResponse = ReturnType< - typeof sanitizeReportCustomersByDateResponse ->; +type SanitizedCustomersByDateResponse = ReturnType< typeof sanitizeReportCustomersByDateResponse >; // Inferred from processing/products.ts -type SanitizedProductsResponse = ReturnType< - typeof sanitizeReportProductsResponse ->; +type SanitizedProductsResponse = ReturnType< typeof sanitizeReportProductsResponse >; // Inferred from processing/visitors.ts -type SanitizedVisitorsResponse = ReturnType< - typeof sanitizeReportVisitorsResponse ->; +type SanitizedVisitorsResponse = ReturnType< typeof sanitizeReportVisitorsResponse >; // Inferred from processing/visitors-by-location.ts type SanitizedVisitorsByLocationResponse = ReturnType< @@ -83,9 +66,7 @@ type SanitizedVisitorsByLocationResponse = ReturnType< >; // Inferred from processing/conversion-rate.ts -type SanitizedConversionRateResponse = ReturnType< - typeof sanitizeReportConversionRateResponse ->; +type SanitizedConversionRateResponse = ReturnType< typeof sanitizeReportConversionRateResponse >; // Inferred from processing/orders-by-product-type.ts type SanitizedOrdersByProductTypeResponse = ReturnType< @@ -93,9 +74,7 @@ type SanitizedOrdersByProductTypeResponse = ReturnType< >; // Inferred from processing/bookings.ts -type SanitizedBookingsResponse = ReturnType< - typeof sanitizeReportBookingsResponse ->; +type SanitizedBookingsResponse = ReturnType< typeof sanitizeReportBookingsResponse >; // Inferred from processing/sessions-by-device.ts type SanitizedSessionsByDeviceResponse = ReturnType< 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 index 2ad7b3fb685d..e6c2ea4e4a58 100644 --- 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 @@ -1,6 +1,7 @@ /** * External dependencies */ +import { tz } from '@date-fns/tz'; import { startOfDay, endOfDay, @@ -12,8 +13,6 @@ import { startOfYear, endOfYear, } from 'date-fns'; -import { tz } from '@date-fns/tz'; - /** * Mocks – getSiteTimezone and dateToISOStringWithLocalTZ * depend on WordPress core store. @@ -25,11 +24,8 @@ import { tz } from '@date-fns/tz'; */ jest.mock( '../date', () => ( { getSiteTimezone: jest.fn( () => '+00:00' ), - dateToISOStringWithLocalTZ: jest.fn( ( date: Date ) => - new Date( date.getTime() ).toISOString() - ), + dateToISOStringWithLocalTZ: jest.fn( ( date: Date ) => new Date( date.getTime() ).toISOString() ), } ) ); - /** * Internal dependencies */ @@ -49,6 +45,10 @@ 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(); } @@ -120,24 +120,16 @@ describe( 'computeDateRangeFromPreset', () => { 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 } ) ) - ); + 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 } ) ) - ); + 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"', () => { @@ -145,9 +137,7 @@ describe( 'computeDateRangeFromPreset', () => { const lastYear = subYears( TODAY_START, 1 ); expect( range ).toBeDefined(); - expect( range!.from ).toBe( - toZ( startOfYear( lastYear, { in: UTC } ) ) - ); + expect( range!.from ).toBe( toZ( startOfYear( lastYear, { in: UTC } ) ) ); expect( range!.to ).toBe( toZ( endOfYear( lastYear, { in: UTC } ) ) ); } ); 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 index 88ca70758db1..1dc771865b48 100644 --- 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 @@ -1,5 +1,5 @@ /** - * Mocks – break the dependency chain to @wordpress/core-data. + * Mocks – break the dependency chain to `@wordpress/core-data`. */ jest.mock( '../../defaults', () => ( { getDefaultQueryParams: jest.fn(), @@ -12,7 +12,6 @@ jest.mock( '../preset-date-range', () => ( { jest.mock( '../interval', () => ( { getDefaultIntervalForPeriod: jest.fn(), } ) ); - /** * Internal dependencies */ 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 index 25850845d3c8..00644be04800 100644 --- 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 @@ -12,14 +12,13 @@ jest.mock( '../preset-date-range', () => ( { jest.mock( '../interval', () => ( { getDefaultIntervalForPeriod: jest.fn(), } ) ); - /** * Internal dependencies */ -import { normalizeReportParams } from '../search'; import { getDefaultQueryParams } from '../../defaults'; -import { computeDateRangeFromPreset } from '../preset-date-range'; 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< @@ -86,9 +85,7 @@ describe( 'normalizeReportParams', () => { // 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_from ).toBe( DEFAULTS_WITH_COMPARISON.compare_from ); expect( result.compare_to ).toBe( DEFAULTS_WITH_COMPARISON.compare_to ); } ); diff --git a/projects/packages/premium-analytics/packages/data/src/utils/date.ts b/projects/packages/premium-analytics/packages/data/src/utils/date.ts index 011e8ae2b768..81f1950d8d4e 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/date.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/date.ts @@ -1,14 +1,14 @@ /** * External dependencies */ -import { select } from '@wordpress/data'; -import { store as coreStore, type Settings } from '@wordpress/core-data'; +import { type TZDate } from '@date-fns/tz'; import { toLocalTZ, formatToTimezoneNaiveString as _formatNaive, dateToISOStringWithTZ as _toISOWithTZ, } from '@jetpack-premium-analytics/datetime'; -import { type TZDate } from '@date-fns/tz'; +import { store as coreStore, type Settings } from '@wordpress/core-data'; +import { select } from '@wordpress/data'; type FullSettings = Settings & { gmt_offset: number; @@ -16,8 +16,7 @@ type FullSettings = Settings & { let DEFAULT_TIME_ZONE: string; try { - DEFAULT_TIME_ZONE = - Intl.DateTimeFormat().resolvedOptions().timeZone ?? '+00:00'; + DEFAULT_TIME_ZONE = Intl.DateTimeFormat().resolvedOptions().timeZone ?? '+00:00'; } catch { DEFAULT_TIME_ZONE = '+00:00'; } @@ -37,9 +36,10 @@ function formatGmtOffset( offset: number | undefined ): string { 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' ) }`; + return `${ sign }${ String( hours ).padStart( 2, '0' ) }:${ String( minutes ).padStart( + 2, + '0' + ) }`; } /* @@ -50,11 +50,11 @@ function formatGmtOffset( offset: number | undefined ): string { * @param {string} timezone - The timezone to use. * @return {string} The timezone. */ +/** + * + */ export function getSiteTimezone() { - const siteSettings = select( coreStore ).getEntityRecord( - 'root', - 'site' - ) as FullSettings; + const siteSettings = select( coreStore ).getEntityRecord( 'root', 'site' ) as FullSettings; if ( ! siteSettings ) { return DEFAULT_TIME_ZONE; @@ -71,14 +71,9 @@ export function getSiteTimezone() { * @return {string} The site's GMT offset. */ export function getSiteGmtOffset(): string { - const siteSettings = select( coreStore ).getEntityRecord( - 'root', - 'site' - ) as FullSettings; + const siteSettings = select( coreStore ).getEntityRecord( 'root', 'site' ) as FullSettings; if ( ! siteSettings ) { - throw new Error( - 'getSiteGmtOffset() called before core settings are ready' - ); + throw new Error( 'getSiteGmtOffset() called before core settings are ready' ); } return formatGmtOffset( siteSettings?.gmt_offset ) || DEFAULT_TIME_ZONE; } @@ -88,11 +83,10 @@ export function getSiteGmtOffset(): string { * - 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 { +export function localTZDate( value?: number | string | Date, timezone?: string ): TZDate { const tz = timezone ?? getSiteTimezone(); return toLocalTZ( value, tz ); } @@ -100,11 +94,10 @@ export function localTZDate( /** * 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 { +export function formatToTimezoneNaiveString( date: Date, timezone?: string ): string { const tz = timezone ?? getSiteTimezone(); return _formatNaive( date, tz ); } @@ -112,11 +105,10 @@ export function formatToTimezoneNaiveString( /** * 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 { +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 index 697202c07c86..cd00c8a6e0e5 100644 --- 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 @@ -1,8 +1,8 @@ /** * External dependencies */ -import { resolveSelect } from '@wordpress/data'; import { store as coreStore } from '@wordpress/core-data'; +import { resolveSelect } from '@wordpress/data'; let readyPromise: Promise< void > | null = null; @@ -14,11 +14,7 @@ export function ensureCoreSettingsReady(): Promise< void > { if ( ! readyPromise ) { readyPromise = Promise.all( [ resolveSelect( coreStore ).getEntityRecord( 'root', 'site' ), - resolveSelect( coreStore ).getEntityRecord( - 'root', - 'settings', - 'general' - ), + resolveSelect( coreStore ).getEntityRecord( 'root', 'settings', 'general' ), ] ).then( () => void 0 ); } return readyPromise; diff --git a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts index e73a9a4e04b2..30c962cb435d 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/interval.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/interval.ts @@ -2,24 +2,23 @@ * External dependencies */ import { differenceInHours } from 'date-fns'; - /** * Internal dependencies */ -import type { IntervalType } from './search'; import { localTZDate } from './date'; +import type { IntervalType } from './search'; -function getAllowedIntervalsByRange( - from: string, - to: string -): IntervalType[] { +/** + * + * @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 - ) + Math.abs( differenceInHours( localTZDate( to ), localTZDate( from ) ) / 24 ) ); if ( daysDiff >= 1095 ) { @@ -42,6 +41,9 @@ function getAllowedIntervalsByRange( /** * Returns the allowed selectable intervals for a specific period. * + * @param period + * @param from + * @param to * @return {Array} Array containing allowed intervals. */ function getAllowedIntervalsForPeriod( @@ -69,6 +71,12 @@ function getAllowedIntervalsForPeriod( } } +/** + * + * @param period + * @param from + * @param to + */ export function getDefaultIntervalForPeriod( period: string | undefined, from: string, @@ -77,6 +85,12 @@ export function getDefaultIntervalForPeriod( 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, diff --git a/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts index 3ff816683937..8a4535a7231d 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/parsing.ts @@ -1,5 +1,7 @@ /** * Safe integer parsing with fallback value + * @param value + * @param fallback */ export function safeParseInt( value: unknown, fallback = 0 ): number { const num = parseInt( String( value ), 10 ); @@ -8,6 +10,8 @@ export function safeParseInt( value: unknown, fallback = 0 ): number { /** * Safe float parsing with fallback value + * @param value + * @param fallback */ export function safeParseFloat( value: unknown, fallback = 0 ): number { const num = parseFloat( String( value ) ); 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 index d3695405408c..cf0345d1003d 100644 --- 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 @@ -2,12 +2,11 @@ * External dependencies */ import { computePrimaryRange } from '@jetpack-premium-analytics/datetime'; +import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date'; import type { SelectablePresetId } from '@jetpack-premium-analytics/datetime'; - /** * Internal dependencies */ -import { getSiteTimezone, dateToISOStringWithLocalTZ } from './date'; /** * Compute the absolute date range for a given preset ID 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 index 7c000b4ea339..063b924a50b2 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/product-filters.ts @@ -19,7 +19,5 @@ export function hasProductFilters( filters?: FilterCondition[] ): boolean { return false; } - return filters.some( ( filter ) => - PRODUCT_FILTER_KEYS.includes( filter.key ) - ); + 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 index 4d01bd951f09..0aa97aacb501 100644 --- a/projects/packages/premium-analytics/packages/data/src/utils/search.ts +++ b/projects/packages/premium-analytics/packages/data/src/utils/search.ts @@ -7,34 +7,27 @@ import { type ComparisonPresetId, type PrimaryPresetId, } from '@jetpack-premium-analytics/datetime'; - /** * Internal dependencies */ -import { getDefaultQueryParams } from '../defaults'; 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 { FilterCondition } from '../types/filter-condition'; 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. + * 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'; +export type IntervalType = 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year'; /* * ReportParams are the expected params present in the client URL. @@ -64,18 +57,15 @@ type PartialComparisonFields = Partial< /* * Checks if the comparison is present in the search params. */ -export function hasComparisonEnabled< T extends PartialComparisonFields >( - p: T -) { - return ( - p.comp === '1' && !! p.compare_from?.trim() && !! p.compare_to?.trim() - ); +/** + * + * @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' -> & { +type NormalizeReportParamsArgType = Omit< ReportParams, 'from' | 'to' | 'interval' | 'preset' > & { from?: string; to?: string; interval?: string; @@ -86,8 +76,8 @@ type NormalizeReportParamsArgType = Omit< * 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. + * @param {NormalizeReportParamsArgType} [search] - URL search params. + * @param {PresetType} [defaultPreset] - Override the fallback preset. */ export function normalizeReportParams( search?: NormalizeReportParamsArgType, From b8a36689ac2b728f4d9c395056489de66bd9c8f2 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:17:19 +0800 Subject: [PATCH 5/7] chore(premium-analytics): wire data package deps and test setup --- pnpm-lock.yaml | 24 +++++++++++++ .../premium-analytics/babel.config.cjs | 8 +++++ .../premium-analytics/eslint.config.mjs | 36 +++++++++++++------ .../packages/premium-analytics/package.json | 11 +++++- .../premium-analytics/tests/jest.config.cjs | 13 +++++++ 5 files changed, 80 insertions(+), 12 deletions(-) create mode 100644 projects/packages/premium-analytics/babel.config.cjs create mode 100644 projects/packages/premium-analytics/tests/jest.config.cjs diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 3c99cde016d7..90e26c323350 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -3827,9 +3827,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) @@ -3842,6 +3851,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 @@ -3852,9 +3864,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 @@ -3864,6 +3885,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/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 742153f28d0a..16c594b5b408 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.13.0", - "browserslist": "4.28.2" + "browserslist": "4.28.2", + "jest": "30.4.2" } } 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' ), + }, +}; From 8ec819e4c1d8f74c12ca71c2fa5af7ae895493d9 Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Fri, 29 May 2026 14:17:43 +0800 Subject: [PATCH 6/7] changelog: add entry for premium-analytics data port --- .../wooa7s-1316-integrate-data-package-into-analytics | 4 ++++ 1 file changed, 4 insertions(+) create mode 100644 projects/packages/premium-analytics/changelog/wooa7s-1316-integrate-data-package-into-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. From 8b2d9b4b7a2e0708c4c533f732c654bffadde64a Mon Sep 17 00:00:00 2001 From: Chi-Hsuan Huang Date: Wed, 3 Jun 2026 11:23:34 +0800 Subject: [PATCH 7/7] docs(premium-analytics): use canonical package name in data README --- .../premium-analytics/packages/data/README.md | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/projects/packages/premium-analytics/packages/data/README.md b/projects/packages/premium-analytics/packages/data/README.md index 7db93ce258ff..c8492a081340 100644 --- a/projects/packages/premium-analytics/packages/data/README.md +++ b/projects/packages/premium-analytics/packages/data/README.md @@ -1,4 +1,4 @@ -# @jetpack-premium-analytics/data +# @automattic/jetpack-premium-analytics-data Data management for Jetpack Premium Analytics with React Query integration. @@ -15,7 +15,7 @@ import { useReport, prefetchReport, // ... other exports -} from '@jetpack-premium-analytics/data'; +} from '@automattic/jetpack-premium-analytics-data'; ``` ## Features @@ -36,7 +36,7 @@ import { ### Setup ```tsx -import { AnalyticsQueryClientProvider } from '@jetpack-premium-analytics/data'; +import { AnalyticsQueryClientProvider } from '@automattic/jetpack-premium-analytics-data'; function App() { return ( @@ -55,7 +55,7 @@ import { useReportOrdersByProductType, useReportOrderAttribution, useReportCoupons -} from '@jetpack-premium-analytics/data'; +} from '@automattic/jetpack-premium-analytics-data'; function OrdersReport() { // Orders endpoint separates primary and comparison periods @@ -105,7 +105,7 @@ function CouponsReport() { ### Prefetching ```tsx -import { prefetchReport, ensureCoreSettingsReady } from '@jetpack-premium-analytics/data'; +import { prefetchReport, ensureCoreSettingsReady } from '@automattic/jetpack-premium-analytics-data'; export const route = { beforeLoad: async () => { @@ -241,7 +241,7 @@ Returns the optimal default interval for a given time period. **Example:** ```tsx -import { getDefaultIntervalForPeriod } from '@jetpack-premium-analytics/data'; +import { getDefaultIntervalForPeriod } from '@automattic/jetpack-premium-analytics-data'; const interval = getDefaultIntervalForPeriod( 'last-7-days', from, to ); // Returns 'day' ``` @@ -254,7 +254,7 @@ Constant array of available order attribution views. **Example:** ```tsx -import { ORDER_ATTRIBUTION_VIEWS } from '@jetpack-premium-analytics/data'; +import { ORDER_ATTRIBUTION_VIEWS } from '@automattic/jetpack-premium-analytics-data'; // Use in components for view selection const views = ORDER_ATTRIBUTION_VIEWS; // ['channel', 'source', ...] @@ -333,7 +333,7 @@ Creates a timezone-aware date using the site's configured timezone by default. ```typescript -import { localTZDate } from '@jetpack-premium-analytics/data'; +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' );