{"id":47608151,"url":"https://github.com/laplace-live/fulfiller","last_synced_at":"2026-04-01T19:40:30.671Z","repository":{"id":342945486,"uuid":"1039094589","full_name":"laplace-live/fulfiller","owner":"laplace-live","description":"Automated fulfillment framework that syncs order shipments from third-party warehouses without native Shopify integration to Shopify using the GraphQL Admin API","archived":false,"fork":false,"pushed_at":"2026-03-16T19:56:29.000Z","size":1320,"stargazers_count":0,"open_issues_count":6,"forks_count":0,"subscribers_count":0,"default_branch":"master","last_synced_at":"2026-03-16T23:53:35.589Z","etag":null,"topics":["bun","fulfillment","graphql","laplace-live","libsql","rouzao","shopify"],"latest_commit_sha":null,"homepage":"","language":"TypeScript","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"agpl-3.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/laplace-live.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null,"publiccode":null,"codemeta":null,"zenodo":null,"notice":null,"maintainers":null,"copyright":null,"agents":null,"dco":null,"cla":null}},"created_at":"2025-08-16T13:21:12.000Z","updated_at":"2026-03-16T11:11:51.000Z","dependencies_parsed_at":null,"dependency_job_id":null,"html_url":"https://github.com/laplace-live/fulfiller","commit_stats":null,"previous_names":["laplace-live/fulfiller"],"tags_count":6,"template":false,"template_full_name":null,"purl":"pkg:github/laplace-live/fulfiller","repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/laplace-live%2Ffulfiller","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/laplace-live%2Ffulfiller/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/laplace-live%2Ffulfiller/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/laplace-live%2Ffulfiller/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/laplace-live","download_url":"https://codeload.github.com/laplace-live/fulfiller/tar.gz/refs/heads/master","sbom_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/laplace-live%2Ffulfiller/sbom","scorecard":null,"host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":286080680,"owners_count":31291178,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2026-04-01T13:12:26.723Z","status":"ssl_error","status_checked_at":"2026-04-01T13:12:25.102Z","response_time":53,"last_error":"SSL_read: unexpected eof while reading","robots_txt_status":"success","robots_txt_updated_at":"2025-07-24T06:49:26.215Z","robots_txt_url":"https://github.com/robots.txt","online":false,"can_crawl_api":true,"host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":["bun","fulfillment","graphql","laplace-live","libsql","rouzao","shopify"],"created_at":"2026-04-01T19:40:30.037Z","updated_at":"2026-04-01T19:40:30.653Z","avatar_url":"https://github.com/laplace-live.png","language":"TypeScript","funding_links":[],"categories":[],"sub_categories":[],"readme":"# LAPLACE Fulfiller\n\nAutomated fulfillment framework that syncs order shipments from third-party warehouses without native Shopify integration to Shopify using the GraphQL Admin API.\n\n## Features\n\n- **Multi-Provider Support**: Extensible architecture supporting multiple fulfillment providers\n- **Automated Monitoring**: Checks provider orders every 5 minutes\n- **Smart Fulfillment**: Only fulfills orders for specific provider warehouse locations\n- **Duplicate Prevention**: Turso database tracks fulfilled orders across all providers\n- **Provider-Specific Logic**: Each provider can have custom order extraction and tracking logic\n- **Type-Safe GraphQL**: Uses Shopify's GraphQL Admin API (2025-07) with automatic type generation\n- **Built-in Providers**:\n  - Rouzao - Chinese fulfillment provider\n  - HiCustom - Chinese POD fulfillment provider\n\n## Prerequisites\n\n- [Bun](https://bun.sh) runtime\n- API credentials for your fulfillment provider(s)\n- Shopify store with API access\n- Shopify locations matching your provider warehouses\n\n## Installation\n\n```bash\nbun install\n```\n\n## Configuration\n\nCreate a `.env` file in the project root with the following variables:\n\n```dotenv\n# Turso Database Configuration (Required)\nTURSO_DATABASE_URL=libsql://your-database-turso.io\nTURSO_AUTH_TOKEN=your-turso-auth-token\n\n# Shopify API Configuration (Required)\nSHOPIFY_API_KEY=your_shopify_api_key_here\nSHOPIFY_API_SECRET=your_shopify_api_secret_here\nSHOPIFY_ACCESS_TOKEN=your_shopify_access_token_here\nSHOPIFY_SHOP_DOMAIN=yourshop.myshopify.com\nSHOPIFY_APP_URL=https://your-app-url.com\n\n# Provider API Configurations\n# Rouzao (automatically enabled when ROUZAO_TOKEN is set)\nROUZAO_TOKEN=your_rouzao_token_here\nROUZAO_LOCATION_IDS=location_id_1,location_id_2  # Optional: Comma-separated Shopify location IDs\n# If not set, defaults to Rouzao's warehouse location IDs\n\n# HiCustom (automatically enabled when API_KEY and API_SECRET are set)\nHICUSTOM_API_KEY=your_hicustom_api_key\nHICUSTOM_API_SECRET=your_hicustom_api_secret\nHICUSTOM_LOCATION_IDS=location_id_1,location_id_2  # Optional: Comma-separated Shopify location IDs\n# HICUSTOM_API_URL=https://api.hicustom.com  # Optional: Override API base URL\n\n# Add more providers as needed\n# PROVIDER3_API_KEY=your_api_key\n# PROVIDER3_API_SECRET=your_secret\n```\n\n### Setting up Turso Database\n\n1. **Install Turso CLI**:\n\n   ```bash\n   curl -sSfL https://get.tur.so/install.sh | bash\n   ```\n\n2. **Create a database**:\n\n   ```bash\n   turso auth signup  # or turso auth login\n   turso db create laplace-fulfiller\n   ```\n\n3. **Get database credentials**:\n\n   ```bash\n   # Get database URL\n   turso db show laplace-fulfiller --url\n\n   # Create an auth token\n   turso db tokens create laplace-fulfiller\n   ```\n\n4. **Push schema to database**:\n   ```bash\n   bun run db:push\n   ```\n\n**Note**: If migrating from local SQLite, you'll need to export your data from the old database and import it into Turso. The schema remains compatible.\n\n### Getting Rouzao Token\n\n1. Log in to Rouzao (https://www.rouzao.com)\n2. Open browser developer tools (F12)\n3. Go to Network tab\n4. Perform any action that calls the API\n5. Look for the `Rouzao-Token` header in the request\n\n### Getting HiCustom Credentials\n\n1. Log in to HiCustom (https://www.hicustom.com)\n2. Navigate to API settings or developer section\n3. Create a new application or API client\n4. Copy the API Key and API Secret\n5. Note your Shopify location IDs that HiCustom will fulfill from\n\nThe HiCustom integration uses their OAuth API with automatic token refresh. See their API documentation:\n\n- [获取access_token](http://xiaoyaoji.cn/project/1jPL8Hr5Xf7/1jUEaURCXKK)\n- [刷新access_token](http://xiaoyaoji.cn/project/1jPL8Hr5Xf7/1kErqGf2swS)\n\n### Setting up Shopify API Access\n\n1. Create a private app in your Shopify admin\n2. Grant the following permissions:\n   - Read orders (`read_orders`)\n   - Write orders (`write_orders`)\n   - Read locations (`read_locations`) - optional, but recommended\n   - Read merchant-managed fulfillment orders (`read_merchant_managed_fulfillment_orders`)\n   - Write merchant-managed fulfillment orders (`write_merchant_managed_fulfillment_orders`)\n3. Copy the API credentials\n\n**Important**: The fulfillment order permissions are required for the app to work properly. Without these permissions, you'll receive 403 Forbidden errors when attempting to process orders.\n\n### Shopify Location Setup\n\nEach provider must have corresponding locations in Shopify. The provider will only fulfill orders from its registered locations.\n\nYou can run `bun run diagnose` to get the location IDs in your Shopify store.\n\n**For Rouzao**:\n\n- Set specific location IDs in `ROUZAO_LOCATION_IDS` environment variable\n- Or create locations with names containing \"Rouzao\"\n\n**For HiCustom**:\n\n- Set specific location IDs in `HICUSTOM_LOCATION_IDS` environment variable\n- Or create locations with names containing \"HiCustom\"\n\n**For other providers**: Check the provider's `locationIds` or location name patterns\n\n## Running\n\n### Development\n\n```bash\n# Run with cron (continuous mode)\nbun run start\n\n# Run once and exit\nbun run once\n\n# Or with the flag directly\nbun run src/index.ts --once\n```\n\n### Production\n\nDeploy this application using the officially maintained container image from GitHub Container Registry. This is the only supported deployment method to ensure consistency, security, and compatibility.\n\n#### Using the Official Image\n\n```bash\n# Pull the latest image\ndocker pull ghcr.io/laplace-live/fulfiller:latest\n\n# Run the container\ndocker run -d \\\n  --name laplace-fulfiller \\\n  --restart unless-stopped \\\n  --env-file .env \\\n  ghcr.io/laplace-live/fulfiller:latest\n```\n\n#### Docker Compose Example\n\nCreate a `docker-compose.yml` file for easier deployment:\n\n```yaml\nservices:\n  fulfiller:\n    image: ghcr.io/laplace-live/fulfiller:latest\n    restart: unless-stopped\n    env_file: .env\n```\n\nThen run:\n\n```bash\ndocker-compose up -d\n```\n\n#### Container Registry\n\nThe official image is publicly available at GitHub Container Registry:\n\n```bash\n# Pull the official image\ndocker pull ghcr.io/laplace-live/fulfiller:latest\n\n# View available tags and versions\n# Visit: https://github.com/laplace-live/fulfiller/pkgs/container/fulfiller\n```\n\nThe image is automatically built and published with each release, ensuring you always have access to the latest stable version.\n\n## How It Works\n\n1. **Provider Registration**: On startup, all enabled providers are registered and initialized\n2. **Order Monitoring**: Every 5 minutes, the service fetches orders from all enabled providers\n3. **Shipped Order Detection**: Each provider filters its orders based on shipped status\n4. **Order Details**: For each shipped order, fetches detailed information including tracking\n5. **Shopify Order Lookup**: Each provider extracts the Shopify order number using its own logic\n6. **Smart Fulfillment**: Only fulfills items assigned to the provider's specific warehouse locations\n7. **Duplicate Prevention**: Records fulfilled orders in Turso database with provider context\n\n## Database\n\nThe service uses Turso (distributed SQLite) with Drizzle ORM to track fulfilled orders across all providers:\n\n**Schema**:\n\n- `provider` - Provider identifier (e.g., 'rouzao')\n- `provider_order_id` - Provider's order ID\n- `shopify_order_number` - Shopify order number\n- `shopify_order_id` - Shopify order ID\n- `fulfilled_at` - Fulfillment timestamp\n- `created_at` - Record creation timestamp\n\n**Features**:\n\n- Unique constraint on `(provider, provider_order_id)` prevents duplicates\n- Indexed by provider and Shopify order number for fast lookups\n- Old records (\u003e365 days) are automatically cleaned up daily at midnight\n- Type-safe queries with Drizzle ORM\n- Global edge deployment with Turso for low-latency access\n- Automatic backups and point-in-time recovery\n- No file size limits unlike local SQLite\n\n## Logging\n\nAll activities are logged with ISO timestamps. Monitor the console output for:\n\n- Order fetch results\n- Shipped order processing\n- Fulfillment success/failure\n- Error messages\n\n## Troubleshooting\n\n### Common Issues\n\n1. **\"Rouzao location not found\"**: Ensure you have a location named \"Rouzao\" or \"柔造\" in Shopify\n2. **\"Order already fulfilled\"**: The order has already been processed or fulfilled in Shopify\n3. **\"Invalid third party order SN format\"**: The order doesn't have a valid Shopify reference\n4. **API errors**: Check your API tokens and network connectivity\n\n### Debug Mode\n\nTo see more detailed logs, you can modify the console.log statements in the code or add additional logging.\n\n### Diagnostic Tool\n\nRun the diagnostic script to check your Shopify API permissions and connections:\n\n```bash\nbun run diagnose\n```\n\nThis will test:\n\n- Environment variable configuration\n- Shopify API authentication\n- Access scopes granted to your app\n- Locations API access (lists all warehouse locations)\n- Orders and Fulfillment Orders API access\n- Rouzao location availability\n\n## Development\n\n### Tech Stack\n\n- **Runtime**: Bun (fast all-in-one JavaScript runtime)\n- **Language**: TypeScript with strict mode\n- **Database**: Turso (distributed SQLite) with Drizzle ORM and @libsql/client\n- **API**: Shopify GraphQL Admin API (2025-07)\n- **Type Generation**: GraphQL Code Generator with Shopify preset\n- **Scheduling**: Croner for cron jobs\n- **Code Quality**: Prettier with import sorting\n- **Containerization**: Production-ready Docker images available at GitHub Container Registry\n\n### GraphQL Type Generation\n\nThe project automatically generates TypeScript types from GraphQL queries:\n\n```bash\n# Generate types once\nbun run graphql-codegen\n\n# Watch mode for development (not configured)\n# bun run graphql-codegen:watch\n```\n\nGenerated types are stored in `src/types/admin.generated.d.ts` and should not be edited manually.\n\n### Database Management\n\nUsing Drizzle ORM for type-safe database operations:\n\n```bash\n# Generate migrations\nbun run db:generate\n\n# Apply migrations\nbun run db:migrate\n\n# Push schema changes directly (development)\nbun run db:push\n\n# Open Drizzle Studio (visual database browser)\nbun run db:studio\n```\n\n### Project Structure\n\n```\n├── src/\n│   ├── index.ts                    # Main application entry point\n│   ├── lib/\n│   │   ├── carriers.ts            # Centralized carrier configuration\n│   │   ├── db/\n│   │   │   ├── client.ts          # Database client and operations\n│   │   │   └── schema.ts          # Drizzle ORM schema definition\n│   │   ├── providers/\n│   │   │   ├── registry.ts        # Provider registration and management\n│   │   │   ├── rouzao.ts          # Rouzao provider implementation\n│   │   │   ├── hicustom.ts        # HiCustom provider implementation\n│   │   │   └── example.ts         # Example provider template\n│   │   ├── queries.graphql.ts     # GraphQL queries and mutations\n│   │   └── shopify.ts             # Shopify GraphQL API integration\n│   ├── scripts/\n│   │   └── diagnose.ts            # Diagnostic tool\n│   ├── types/\n│   │   ├── index.ts               # Provider interfaces and types\n│   │   ├── rouzao.ts              # Rouzao-specific types\n│   │   ├── hicustom.ts            # HiCustom-specific types\n│   │   └── admin.generated.d.ts   # Auto-generated GraphQL types\n│   └── utils/                     # Utility functions\n├── drizzle/                        # Database migrations\n├── references/                     # Reference implementations (gitignored)\n├── package.json\n├── tsconfig.json                   # TypeScript config with path aliases\n├── drizzle.config.ts              # Drizzle ORM configuration\n├── .graphqlrc.ts                  # GraphQL code generation config\n└── .prettierrc.mjs                # Code formatting config\n```\n\n### Multi-Provider Architecture\n\nThe application uses a provider-based architecture that makes it easy to add support for new fulfillment providers.\n\n#### Provider Interface\n\nEach provider must implement the `Provider` interface:\n\n```typescript\ninterface Provider {\n  id: string; // Unique provider identifier\n  name: string; // Human-readable name\n  locationIds: string[]; // Shopify location IDs managed by this provider\n\n  // Check if provider has required configuration\n  isConfigured(): boolean;\n\n  // Check if a location belongs to this provider\n  isProviderLocation(locationName: string, locationId: string): boolean;\n\n  // Fetch shipped orders from the provider\n  fetchShippedOrders(): Promise\u003cProviderOrder[]\u003e;\n\n  // Fetch detailed order information\n  fetchOrderDetail(orderId: string): Promise\u003cProviderOrderDetail | null\u003e;\n\n  // Extract Shopify order number from provider's data\n  extractShopifyOrderNumber(orderDetail: ProviderOrderDetail): string | null;\n\n  // Get tracking information\n  getTrackingInfo(orderDetail: ProviderOrderDetail): TrackingInfo;\n}\n```\n\n#### Adding a New Provider\n\n1. **Copy the example template**:\n\n   ```bash\n   cp src/lib/providers/example.ts src/lib/providers/myprovider.ts\n   ```\n\n2. **Implement your provider logic**:\n   - Update API endpoints and authentication\n   - Map your provider's data structure\n   - Configure carrier mappings and tracking URLs\n   - Add your Shopify location IDs\n\n3. **Register the provider** in `src/lib/providers/registry.ts`:\n\n   ```typescript\n   import { myProvider } from \"./myprovider\";\n\n   // In the constructor\n   this.register(myProvider);\n   ```\n\n4. **Add environment variables** to `.env`:\n\n   ```dotenv\n   MYPROVIDER_API_KEY=your-api-key\n   MYPROVIDER_API_SECRET=your-secret\n   ```\n\n5. **Run the application** - your provider will be automatically included!\n\n#### Provider Features\n\n- **Automatic order tracking**: Each provider's orders are tracked separately\n- **Custom business logic**: Providers can implement their own order number extraction patterns\n- **Centralized carrier system**: All providers share a unified carrier configuration\n- **Flexible carrier aliases**: Different providers can use different codes for the same carrier\n- **Automatic tracking URLs**: Generate tracking URLs based on carrier and tracking number\n- **Error isolation**: Errors in one provider don't affect others\n\n#### Centralized Carrier Configuration\n\nThe application uses a centralized carrier system (`src/lib/carriers.ts`) that:\n\n1. **Defines carriers once**: Each carrier has a name and tracking URL template\n2. **Supports multiple aliases**: Different providers can use different codes/names for the same carrier\n3. **Provides unified lookup**: All providers use the same functions to get carrier info\n\nExample:\n\n```typescript\n// Rouzao uses 'sf' for SF Express\n// HiCustom uses '顺丰速运' for SF Express\n// Both resolve to the same carrier with tracking URL\n\nconst trackingDetails = getTrackingDetails(\"sf\", \"123456\");\n// or\nconst trackingDetails = getTrackingDetails(\"顺丰速运\", \"123456\");\n\n// Both return:\n// {\n//   carrierName: 'SF Express',\n//   trackingUrl: 'https://www.sf-express.com/.../123456'\n// }\n```\n\nTo add support for a new carrier or alias:\n\n1. Edit `src/lib/carriers.ts`\n2. Find the carrier in the `CARRIERS` array\n3. Add your provider's code/name to the `aliases` array\n\n#### Provider Configuration\n\nProviders are automatically enabled or disabled based on their configuration:\n\n**Automatic Detection**:\n\n- If required environment variables are set → Provider is enabled\n- If required environment variables are missing → Provider is disabled\n\nFor example:\n\n```dotenv\n# Rouzao will be enabled (has required token)\nROUZAO_TOKEN=abc123\nROUZAO_LOCATION_IDS=gid://shopify/Location/123,gid://shopify/Location/456  # Optional\n\n# HiCustom will be enabled (has required credentials)\nHICUSTOM_API_KEY=client123\nHICUSTOM_API_SECRET=secret456\nHICUSTOM_LOCATION_IDS=gid://shopify/Location/789,gid://shopify/Location/101  # Optional\n\n# Example provider will be disabled (missing required key)\n# EXAMPLE_API_KEY=\n```\n\n**How it Works**:\nEach provider implements an `isConfigured()` method that checks for required environment variables:\n\n```typescript\nclass MyProvider implements Provider {\n  isConfigured(): boolean {\n    return !!process.env[\"MYPROVIDER_API_KEY\"];\n  }\n}\n```\n\n### Adding Features\n\n- To modify the polling interval, change the cron expression in `src/index.ts`\n- To add new GraphQL queries, edit `src/lib/queries.graphql.ts` and run `bun run graphql-codegen`\n- To support additional carriers, update the mapping in your provider implementation\n- All imports use the `@/` path alias (e.g., `import { Provider } from '@/types'`)\n\n## License\n\nAGPL-3.0\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaplace-live%2Ffulfiller","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Flaplace-live%2Ffulfiller","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Flaplace-live%2Ffulfiller/lists"}