&per_page=5&orderby=date&order=desc * POST orders JSON body (see create_order below) → respond 201 {id, number, status, total, line_items} * Every request carries ?consumer_key=&consumer_secret= — reject with 401 when they do not match. * Guide (বাংলা): https://alapi.chat/website-alapi-connect-woocommerce-laravel-nextjs/ */ declare( strict_types=1 ); const ALAPI_CK = 'ck_alapi_demo'; const ALAPI_CS = 'cs_alapi_demo'; const BASE_URL = 'https://alapi.chat/alapi-sample-store'; const DATA_DIR = '/home/alapi.chat/alapi-private/sample-store'; // outside the web root const MAX_ORDERS = 500; // demo store keeps the last 500 orders only $route = isset( $_GET['route'] ) ? trim( (string) $_GET['route'], '/' ) : ''; $method = strtoupper( (string) ( $_SERVER['REQUEST_METHOD'] ?? 'GET' ) ); /* ---------------------------------------------------------------- helpers */ function out( $data, int $code = 200 ): void { http_response_code( $code ); header( 'Content-Type: application/json; charset=utf-8' ); header( 'X-Alapi-Sample-Store: 1' ); echo json_encode( $data, JSON_UNESCAPED_UNICODE | JSON_UNESCAPED_SLASHES ); exit; } /** WooCommerce-style error envelope. */ function wc_error( string $code, string $message, int $status ): void { out( array( 'code' => $code, 'message' => $message, 'data' => array( 'status' => $status ) ), $status ); } function catalog(): array { return array( array( 'id' => 101, 'name' => 'নমুনা টি-শার্ট (Sample T-Shirt)', 'price' => 450, 'regular_price' => 550, 'stock' => 25, 'image' => 'tshirt.jpg', 'short' => '১০০% কটন, M/L/XL — নমুনা পণ্য, আসল নয়।' ), array( 'id' => 102, 'name' => 'নমুনা শাড়ি (Sample Saree)', 'price' => 1290, 'regular_price' => 1290, 'stock' => 8, 'image' => 'saree.jpg', 'short' => 'হাফ-সিল্ক, ১২ হাত — নমুনা পণ্য।' ), array( 'id' => 103, 'name' => 'নমুনা ঘড়ি (Sample Watch)', 'price' => 790, 'regular_price' => 990, 'stock' => 0, 'image' => 'watch.jpg', 'short' => 'স্টকে নেই — outofstock কেমন দেখায় তার নমুনা।' ), array( 'id' => 104, 'name' => 'নমুনা মধু ৫০০ গ্রাম (Sample Honey)', 'price' => 650, 'regular_price' => 650, 'stock' => 40, 'image' => 'honey.jpg', 'short' => 'সুন্দরবনের খাঁটি মধু — নমুনা পণ্য।' ), ); } function product_json( array $p ): array { return array( 'id' => (int) $p['id'], 'name' => (string) $p['name'], 'slug' => 'sample-' . (int) $p['id'], 'type' => 'simple', 'status' => 'publish', 'price' => (string) $p['price'], // string, digits only — Alapi reads this 'regular_price' => (string) $p['regular_price'], 'sale_price' => ( $p['price'] < $p['regular_price'] ) ? (string) $p['price'] : '', 'stock_status' => ( (int) $p['stock'] > 0 ) ? 'instock' : 'outofstock', // Alapi reads this 'stock_quantity' => (int) $p['stock'], 'short_description' => '
' . $p['short'] . '
', // Alapi strips tags 'description' => '' . $p['short'] . '
', 'images' => array( array( 'id' => (int) $p['id'], 'src' => BASE_URL . '/images/' . $p['image'], 'alt' => (string) $p['name'] ) ), // images[0].src → bot photo 'categories' => array(), 'attributes' => array(), 'variations' => array(), ); } function find_product( int $id ): ?array { foreach ( catalog() as $p ) { if ( (int) $p['id'] === $id ) { return $p; } } return null; } function orders_path(): string { if ( ! is_dir( DATA_DIR ) ) { @mkdir( DATA_DIR, 0700, true ); } return DATA_DIR . '/orders.json'; } /** Read under a shared lock (a writer holds LOCK_EX while it truncates + rewrites). */ function load_orders(): array { $f = orders_path(); if ( ! is_file( $f ) ) { return array(); } $h = fopen( $f, 'rb' ); if ( ! $h ) { return array(); } flock( $h, LOCK_SH ); $raw = stream_get_contents( $h ); flock( $h, LOCK_UN ); fclose( $h ); $a = json_decode( (string) $raw, true ); return is_array( $a ) ? $a : array(); } /** One exclusive lock across the whole read-modify-write; a failed encode never overwrites the file. */ function with_orders_locked( callable $fn ) { $h = fopen( orders_path(), 'c+b' ); if ( ! $h ) { wc_error( 'store_io', 'Order storage unavailable.', 500 ); } flock( $h, LOCK_EX ); $orders = json_decode( (string) stream_get_contents( $h ), true ); if ( ! is_array( $orders ) ) { $orders = array(); } $result = $fn( $orders ); $json = json_encode( array_slice( $orders, -MAX_ORDERS ), JSON_UNESCAPED_UNICODE ); if ( false === $json ) { flock( $h, LOCK_UN ); fclose( $h ); wc_error( 'store_encode', 'Could not save the order.', 500 ); } ftruncate( $h, 0 ); rewind( $h ); fwrite( $h, $json ); fflush( $h ); flock( $h, LOCK_UN ); fclose( $h ); return $result; } /** The demo keys are public, so stored orders are public too: mask phone and address in every response. */ function public_order( array $o ): array { $ph = (string) ( $o['billing']['phone'] ?? '' ); if ( strlen( $ph ) > 6 ) { $o['billing']['phone'] = substr( $ph, 0, 3 ) . str_repeat( '*', strlen( $ph ) - 6 ) . substr( $ph, -3 ); } if ( isset( $o['billing']['address_1'] ) ) { $o['billing']['address_1'] = '*** (hidden in the sample store)'; } return $o; } function now_iso(): string { return gmdate( 'Y-m-d\TH:i:s', time() + 6 * 3600 ); } // WC sends site-local time, no TZ suffix /* ---------------------------------------------------------------- landing page (no auth) */ if ( '' === $route && 'GET' === $method ) { header( 'Content-Type: text/html; charset=utf-8' ); echo 'এটা একটা নমুনা স্টোর — Alapi যে WooCommerce-কম্প্যাটিবল API দিয়ে স্টোরের সাথে কথা বলে, তার হুবহু রেফারেন্স। Laravel / Next.js / যেকোনো ব্যাকএন্ডে এই ৪টা রুট বানালেই Alapi আপনার সাইটকে স্টোর হিসেবে নেবে।
' . '📘 গাইড: website-alapi-connect-woocommerce-laravel-nextjs
' . 'Store URL: ' . BASE_URL . "\nConsumer key: " . ALAPI_CK . "\nConsumer secret: " . ALAPI_CS . '' . '
GET ' . BASE_URL . '/wp-json/wc/v3/products?per_page=50&status=publish&consumer_key=' . ALAPI_CK . '&consumer_secret=' . ALAPI_CS . "\nGET " . BASE_URL . '/wp-json/wc/v3/products/101?consumer_key=…&consumer_secret=…' . "\nGET " . BASE_URL . '/wp-json/wc/v3/orders?search=017XXXXXXXX&per_page=5&orderby=date&order=desc&consumer_key=…&consumer_secret=…' . "\nPOST " . BASE_URL . '/wp-json/wc/v3/orders?consumer_key=…&consumer_secret=… (JSON body)' . '
সোর্স (এক ফাইলের PHP, ফ্রেমওয়ার্ক ছাড়া): source.txt · Laravel ও Next.js অ্যাডাপ্টার: alapi-store-adapters.zip
' . 'নমুনা স্টোরে পাঠানো অর্ডার শুধু ডেমো — কেউ ডেলিভারি করবে না। শেষ ৫০০টি রাখা হয়।
'; exit; } /* ---------------------------------------------------------------- auth (every API call) */ $ck = (string) ( $_GET['consumer_key'] ?? '' ); $cs = (string) ( $_GET['consumer_secret'] ?? '' ); if ( ! hash_equals( ALAPI_CK, $ck ) || ! hash_equals( ALAPI_CS, $cs ) ) { wc_error( 'woocommerce_rest_cannot_view', 'consumer_key / consumer_secret ভুল — Sorry, you cannot list resources.', 401 ); } /* ---------------------------------------------------------------- GET products */ if ( 'GET' === $method && 'products' === $route ) { $rows = catalog(); // status=publish is what Alapi sends; the demo catalog is all published. (Filter drafts out in a real store.) if ( isset( $_GET['include'] ) && '' !== trim( (string) $_GET['include'] ) ) { $ids = array_map( 'intval', explode( ',', (string) $_GET['include'] ) ); $rows = array_values( array_filter( $rows, fn( $p ) => in_array( (int) $p['id'], $ids, true ) ) ); } if ( isset( $_GET['search'] ) && '' !== trim( (string) $_GET['search'] ) ) { $q = mb_strtolower( trim( (string) $_GET['search'] ) ); $rows = array_values( array_filter( $rows, fn( $p ) => false !== mb_strpos( mb_strtolower( $p['name'] . ' ' . $p['short'] ), $q ) ) ); } $per = max( 1, min( 100, (int) ( $_GET['per_page'] ?? 10 ) ) ); $page = max( 1, (int) ( $_GET['page'] ?? 1 ) ); $total = count( $rows ); $rows = array_slice( $rows, ( $page - 1 ) * $per, $per ); header( 'X-WP-Total: ' . $total ); header( 'X-WP-TotalPages: ' . (int) ceil( $total / $per ) ); out( array_map( 'product_json', $rows ) ); } /* ---------------------------------------------------------------- GET products/{id} */ if ( 'GET' === $method && preg_match( '#^products/(\d+)$#', $route, $m ) ) { $p = find_product( (int) $m[1] ); if ( ! $p ) { wc_error( 'woocommerce_rest_product_invalid_id', 'Invalid ID.', 404 ); } out( product_json( $p ) ); } /* ---------------------------------------------------------------- GET orders (tracking) */ if ( 'GET' === $method && 'orders' === $route ) { $orders = load_orders(); $search = trim( (string) ( $_GET['search'] ?? '' ) ); if ( '' !== $search ) { $digits = preg_replace( '/\D+/', '', $search ); $orders = array_values( array_filter( $orders, function ( $o ) use ( $search, $digits ) { if ( (string) $o['number'] === $search || (string) $o['id'] === $search ) { return true; } $ph = preg_replace( '/\D+/', '', (string) ( $o['billing']['phone'] ?? '' ) ); return '' !== $digits && strlen( $digits ) >= 6 && false !== strpos( $ph, $digits ); } ) ); } usort( $orders, fn( $a, $b ) => strcmp( (string) $b['date_created'], (string) $a['date_created'] ) ); // newest first if ( 'asc' === strtolower( (string) ( $_GET['order'] ?? 'desc' ) ) ) { $orders = array_reverse( $orders ); } $per = max( 1, min( 100, (int) ( $_GET['per_page'] ?? 10 ) ) ); out( array_map( 'public_order', array_slice( $orders, 0, $per ) ) ); } /* ---------------------------------------------------------------- POST orders (the bot took an order) */ if ( 'POST' === $method && 'orders' === $route ) { $raw_body = (string) file_get_contents( 'php://input', false, null, 0, 65537 ); if ( strlen( $raw_body ) > 65536 ) { wc_error( 'rest_body_too_large', 'Body larger than 64KB.', 413 ); } $body = json_decode( $raw_body, true, 8 ); // the WooCommerce order shape needs depth 4 if ( ! is_array( $body ) ) { wc_error( 'rest_invalid_json', 'Invalid JSON body.', 400 ); } $billing = is_array( $body['billing'] ?? null ) ? $body['billing'] : array(); $items = array(); $subtotal = 0; foreach ( (array) ( $body['line_items'] ?? array() ) as $li ) { $p = find_product( (int) ( $li['product_id'] ?? 0 ) ); if ( ! $p ) { continue; } // unknown product id → skipped (WooCommerce would 400; either is fine for Alapi) $qty = max( 1, (int) ( $li['quantity'] ?? 1 ) ); $line = (int) $p['price'] * $qty; $subtotal += $line; $items[] = array( 'id' => count( $items ) + 1, 'product_id' => (int) $p['id'], 'variation_id' => 0, 'name' => $p['name'], 'quantity' => $qty, 'price' => (float) $p['price'], 'subtotal' => (string) $line, 'total' => (string) $line ); } if ( empty( $items ) ) { wc_error( 'woocommerce_rest_invalid_product_id', 'No valid line_items (product_id not found).', 400 ); } $shipping = 0; foreach ( (array) ( $body['shipping_lines'] ?? array() ) as $sl ) { $shipping += (int) round( (float) ( $sl['total'] ?? 0 ) ); } // Alapi sends the delivery charge here $fees = 0; foreach ( (array) ( $body['fee_lines'] ?? array() ) as $fl ) { $fees += (int) round( (float) ( $fl['total'] ?? 0 ) ); } // negative = bot haggle discount $keep = function ( $lines ) { $o = array(); foreach ( (array) $lines as $l ) { if ( is_array( $l ) ) { $r = array(); foreach ( array( 'method_id', 'method_title', 'name', 'total', 'tax_status' ) as $k ) { if ( isset( $l[ $k ] ) && is_scalar( $l[ $k ] ) ) { $r[ $k ] = mb_substr( (string) $l[ $k ], 0, 120 ); } } $o[] = $r; } } return array_slice( $o, 0, 5 ); }; $cut = function ( $v, $n ) { return mb_substr( is_scalar( $v ) ? (string) $v : '', 0, $n ); }; $order = with_orders_locked( function ( array &$orders ) use ( $body, $billing, $items, $subtotal, $shipping, $fees, $keep, $cut ) { $maxid = 5500; foreach ( $orders as $o ) { $maxid = max( $maxid, (int) $o['id'] ); } $id = $maxid + 1; $order = array( 'id' => $id, 'number' => (string) $id, // Alapi shows this to the customer 'status' => in_array( (string) ( $body['status'] ?? '' ), array( 'pending', 'processing', 'on-hold' ), true ) ? (string) $body['status'] : 'processing', 'currency' => 'BDT', 'date_created' => now_iso(), 'payment_method' => (string) ( $body['payment_method'] ?? 'cod' ), 'payment_method_title' => (string) ( $body['payment_method_title'] ?? 'Cash on Delivery' ), 'set_paid' => false, 'billing' => array( 'first_name' => $cut( $billing['first_name'] ?? '', 160 ), 'last_name' => $cut( $billing['last_name'] ?? '', 160 ), 'phone' => $cut( $billing['phone'] ?? '', 160 ), 'address_1' => $cut( $billing['address_1'] ?? '', 160 ), 'city' => $cut( $billing['city'] ?? '', 160 ), 'country' => (string) ( $billing['country'] ?? 'BD' ), ), 'line_items' => $items, 'shipping_lines' => $keep( $body['shipping_lines'] ?? array() ), 'fee_lines' => $keep( $body['fee_lines'] ?? array() ), 'shipping_total' => (string) $shipping, 'total' => (string) ( $subtotal + $shipping + $fees ), // Alapi mirrors this into its dashboard 'customer_note' => $cut( $body['customer_note'] ?? '', 300 ), ); $orders[] = $order; return $order; } ); out( public_order( $order ), 201 ); } wc_error( 'rest_no_route', 'No route was found matching the URL and request method.', 404 );