member-rewards-update-tool
Keeps member discount data up-to-date by processing recent usage and expired rewards from Shopify exports. Marketing and customer service teams rely on it to ensure customers' reward statuses are accurate and synced with Klaviyo profiles, removing manual work and errors. It also prepares and uploads updated customer files to Shopify and Klaviyo automatically, saving time and keeping rewards consistent.
Member Rewards Update Tool
Processes member discount data from Shopify CSV exports, detects changes against live Klaviyo profiles, and generates output files for Shopify and Klaviyo imports. Changed profiles are uploaded directly to Klaviyo via the bulk import API.
Requirements
- PHP 8.2 or higher with the
curlextension enabled - No Composer or additional dependencies required
Installation
# Clone or extract the project
cd /var/www/alpkit/member-rewards-update-tool
# Create required directories
mkdir -p input output logs cache
chmod 755 input output logs cache
Configuration
config/config.php
Input
| Setting | Description |
|---|---|
input.customers_csv |
Path to the Customers CSV export |
input.discounts_csv |
Path to the Last 7 Days Discount Usage CSV export |
Output
| Setting | Description |
|---|---|
output.directory |
Directory where output files will be saved |
output.archive_directory |
Directory where previous output files are moved on each run |
output.filename_pattern |
Customers XLSX filename — %s is replaced with the current date |
output.customers_copy_to |
Optional path to copy the Customers XLSX to after generation |
output.csv_filename_pattern |
Max Value CSV filename — %s is replaced with the current date |
output.max_value_copy_to |
Optional path to copy the Max Value CSV to after generation |
output.reviews_filename_pattern |
For Reviews CSV filename — %s is replaced with the current date |
output.reviews_copy_to |
Optional path to copy the For Reviews CSV to after generation |
Logging
| Setting | Description |
|---|---|
log.directory |
Directory for log files |
log.level |
Log verbosity: DEBUG, INFO, WARNING, or ERROR |
processing.timezone |
Timezone for date formatting (default: Europe/London) |
Klaviyo profile fetch
| Setting | Description |
|---|---|
klaviyo.enabled |
Set to false to skip Klaviyo comparison and write all rows to CSV |
klaviyo.fetch_mode |
segment — fetch only active profiles from a segment (default); all — fetch all profiles |
klaviyo.segment_id |
Klaviyo segment ID used when fetch_mode is segment (default: XVnnDX — ActiveProfiles) |
klaviyo.suppressed_segment_id |
Klaviyo segment ID used when the --suppressed flag is passed (default: SJPRME — Suppressed contacts) |
klaviyo.cache_dir |
Directory where the fetched profile index is cached |
klaviyo.cache_ttl |
Seconds before the cache is considered stale and re-fetched (default: 86400 — 24 hours) |
Klaviyo profile upload
| Setting | Description |
|---|---|
klaviyo_upload.enabled |
Set to true to upload changed profiles directly to Klaviyo via the bulk import API |
klaviyo_upload.dry_run |
Set to true to log what would be uploaded without making any API calls |
klaviyo_upload.chunk_size |
Profiles per bulk import job (default: 5000, Klaviyo max: 10000) |
klaviyo_upload.retry_limit |
Maximum retries on a 429 rate limit response (default: 5) |
klaviyo_upload.retry_delay_ms |
Initial back-off in milliseconds on a 429, doubles each retry (default: 500) |
config/api.env
Create this file with your Klaviyo API credentials. It is excluded from version control.
KLAVIYO_PRIVATE_KEY=pk_your_private_key_here
The API key requires the following scopes:
profiles:read— for fetching profilessegments:read— for fetching segment membershipprofiles:write— for updating profile propertieslists:write— required by the bulk import endpoint
Input Files
The following CSV files must be in place before running (paths configured in config/config.php):
Customers.csv— full customer export including discount code metafieldsLast7daysDiscountUsage.csv— recent discount usage export
Usage
php process.php [--all|--segment|--suppressed] [--dry-run] [--refresh-cache]
| Flag | Description |
|---|---|
| (none) | Use the fetch_mode set in config/config.php |
--segment |
Override: fetch only active profiles from the configured segment (klaviyo.segment_id) |
--suppressed |
Fetch profiles from the suppressed contacts segment (klaviyo.suppressed_segment_id) instead of the default segment |
--all |
Override: fetch all Klaviyo profiles |
--dry-run |
Enable upload dry-run mode — processes and writes output files normally, but skips the Klaviyo API upload |
--refresh-cache |
Delete the cached profile index and force a fresh fetch from the Klaviyo API |
The script will:
- Load the Klaviyo profile index from cache, or fetch it from the API if the cache is stale
- Load and index both CSV input files
- Match recently used discount codes to customer records and move them from available to used
- Move expired discount codes (expiry date before today) from available to expired
- Sort remaining available codes by value (descending), then expiry date (descending), then alphabetically
- Write the Customers XLSX — records where discount codes have changed (Shopify import)
- Write the Max Value CSV — profiles that exist in Klaviyo and have changed properties (Klaviyo fallback import)
- Write the For Reviews CSV — all customers who have at least one available discount code
- Upload changed profiles directly to Klaviyo via the bulk import API (if
klaviyo_upload.enabledistrue)
Output Files
Three files are written to the output/ directory on each run, each dated with the current date. Previous files are automatically archived to output/archive/.
customers_YYYY-MM-DD.xlsx — Shopify bulk customer import:
- Only contains records where discount codes have changed since the last run
- Columns:
email,Command,Metafield: alpkit.discountcodes_2021,Metafield: alpkit.usedcodes_2021,Metafield: alpkit.expiredcodes_2021
max_value_YYYY-MM-DD.csv — Klaviyo profile import (fallback / audit):
- Only contains profiles that exist in Klaviyo and have at least one changed property
- Columns:
Email,has_dividends,discount_code,discount_value,min_spend,multiple_codes,number_of_codes,balance,account_state,MBR_date_created
for_reviews_YYYY-MM-DD.csv — All customers with active discount codes:
- Only contains customers who have at least one available (non-expired, non-used) discount code after processing
- Columns:
Email,Metafield: alpkit.discountcodes_2021
Each output file can optionally be copied to a secondary location by setting the corresponding copy_to path in config/config.php.
Klaviyo Cache
The fetched Klaviyo profile index is saved to cache/ and reused for 24 hours (configurable via klaviyo.cache_ttl). This avoids a lengthy API fetch on every run.
Cache files are named by fetch mode and segment, so different modes do not share or overwrite each other's cache:
cache/klaviyo_segment_XVnnDX.json— default segment fetch (--segment)cache/klaviyo_segment_SJPRME.json— suppressed contacts fetch (--suppressed)cache/klaviyo_all.json— full profile fetch (--all)
The cache directory is excluded from version control.
Logs
Execution logs are written to logs/process.log. Each run appends a timestamped summary including:
- Number of Klaviyo profiles loaded (from cache or API)
- Processing time and customer counts
- CSV rows written vs skipped (unchanged/not in Klaviyo)
- Klaviyo upload job count, profile count, and any failures
Cron Example
To run daily at 6am:
0 6 * * * ea-php82 -q /home/kennysto/discount-processor-optimized/process.php 2>> /home/kennysto/discount-processor-optimized/logs/process.log
The 2>> redirect appends PHP's own stderr output (fatal errors that bypass the script's error handler) to the same log file used by the script. This ensures nothing is silently discarded if the process is killed unexpectedly.