KOSMOS v1.0 โ ุงูู ูุงุตูุฉ ุงูููุฏุณูุฉ ุงููุงู ูุฉ
ู ู ุงูุชุตูุฑ ุฅูู ุงูุชูููุฐ โ Blueprint ุชูููุฐู ูุงู ู
ูุฐู ููุณุช ู ูุงูุฉ. ูุฐู ู ูุงุตูุฉ ููุฏุณูุฉ (Engineering Spec) ุฌุงูุฒุฉ ููุชุณููู ููุฑูู ุชุทููุฑ. ูู ุฌุฏููุ ูู classุ ูู endpointุ ูู hook โ ู ูุซูู ูู ุจุฑุฑ.
๐ฏ ุงูุฌุฒุก 0: ุฅุนุงุฏุฉ ุชุนุฑูู ุงูู ูุตุฉ โ ุงููุณุฎุฉ ุงูููุงุฆูุฉ
ุงูุงุณู ุงูููุงุฆู: KOSMOS Studio
ุงูููุน: Subscription-as-a-Service Knowledge & Commerce Bridge
ุงูุฑุคูุฉ (ุฌู ูุฉ ูุงุญุฏุฉ):
ุฌุณุฑ ุฑูู ู ูุฑุจุท ุงูู ุนุฑูุฉ ุจู ุงูุชุฌุงุฑุฉ: ู ูุตุฉ ุงุดุชุฑุงููุฉ ุชู ูุญ ุงูุชุฌุงุฑ ูุงูููุงูุงุช ุงููุฏุฑุฉ ุนูู ุณุญุจ ู ูุชุฌุงุชูู ูู ุญุชูุงูู ูู ูุงุฑุงุชูู ู ู ู ุตุงุฏุฑ ู ุชุนุฏุฏุฉุ ูุถุฎููุง ูู ูููุงุช ุงูุจูุน (ุณูุฉุ ุดูุจููุงูุ ููููู ุฑุณ) โ ู ุน ู ุฏุฑูุจ AI ูู ูุชุจุฉ ู ุนุฑูุฉ ุญูุฉ ุชูุบูู ุชููุงุฆูุงู ุนูุฏ ุงููุทุงุน ุงูุงุดุชุฑุงู.
ุงูููู ุฉ ุงููุฑูุฏุฉ (3 ู ุญุงูุฑ):
| ุงูู ุญูุฑ | ุงููุตู |
|---|---|
| ๐ Content Bridge | ุงุณุญุจ ู ู ุฃู ู ุตุฏุฑ (Shopifyุ WooCommerceุ AliExpressุ CSVุ API)ุ ุงุฏูุน ุฅูู ุณูุฉ/ุบูุฑูุง ุจุถุบุทุฉ |
| ๐ง Knowledge Layer | ูู ู ูุชุฌ ูุชุญูู ุฅูู ุนูุฏุฉ ู ุนุฑูุฉ + AI Tutor ูุดุฑุญ ุงุณุชุฎุฏุงู ู |
| ๐ฐ Subscription Lock | ุฅุฐุง ุชููู ุงูุงุดุชุฑุงู โ ุงูู ูุชุฌุงุช ูุงูุชุตูููุงุช ูุงูุฎุฏู ุงุช ุชูููู ููุฑุงู |
ุงูุดุนุงุฑ ุงูุชุณูููู:
“ู ุนุฑูุชูุ ู ูุชุฌุงุชูุ ุฃุฑุจุงุญู โ ูู ู ุฏุงุฑ ูุงุญุฏ. ุชููู ุงูุฏูุนุ ูุชููู ุงูููู.”
ุงูุฌู ููุฑ ุงูู ุณุชูุฏู:
- ุงูุชุงุฌุฑ ูู ุณูุฉย ุงูุฐู ูุฑูุฏ ุงุณุชูุฑุงุฏ ู ูุชุฌุงุช ู ู ู ูุตุงุช ุฃุฎุฑู ุจุฐูุงุก
- ุงูููุงูุฉ ุงูุฑูู ูุฉย ุงูุชู ุชุฏูุฑ ู ุชุงุฌุฑ ู ุชุนุฏุฏุฉ ูุนู ูุงุฆูุง
- ุตุงุญุจ ุงูู ุนุฑูุฉ/ุงูู ุฏุฑุจย ุงูุฐู ูุฑูุฏ ุจูุน ุฏูุฑุงุช + ู ูุชุฌุงุช ุฑูู ูุฉ
- ุงูู ุจุฑู ุฌ/ุงูู ุตู ู ย ุงูุฐู ูุฑูุฏ ุจูุน ููุงูุจู ูุฎุฏู ุงุชู ุจุงุดุชุฑุงู
๐ ุงูุฌุฒุก 1: ุงูู ุนู ุงุฑูุฉ ุงูุนุงู ุฉ (System Architecture)
ุงูู ูููุงุช ุงูุฃุณุงุณูุฉ (Bounded Contexts):
| # | ุงูู ููู | ุงูู ุณุคูููุฉ |
|---|---|---|
| 1 | Tenant Service | ุฅุฏุงุฑุฉ ุงูุญุณุงุจุงุชุ ุงููุทุงูุงุชุ ุงูููุงุชูุฑ |
| 2 | Subscription Engine | ุงูุจุงูุงุชุ ุงูุชุฌุฏูุฏุ ุงูุฅูุบุงุกุ Dunning |
| 3 | Entitlements Engine | “ูู ูุฐุง ุงูู ุณุชุฎุฏู ูุญู ูู ุงููุตููุ” โ ููุทุฉ ุงููุฑุงุฑ ุงูู ุฑูุฒูุฉ |
| 4 | Content Bridge | ุณุญุจ/ุฏูุน ุงูู ูุชุฌุงุช ุจูู ุงูู ุตุงุฏุฑ |
| 5 | AI Gateway | ุชุฌู ูุน ูู ุงุณุชุฏุนุงุกุงุช AI + Metering |
| 6 | Knowledge Graph | ุฑุณู ุงูู ูุงุฑุงุช ูุงูู ูุชุฌุงุช ูุงูุนูุงูุงุช |
| 7 | Job Queue | ุงูู ูุงู ุบูุฑ ุงูู ุชุฒุงู ูุฉ (Sync, Publish) |
| 8 | Audit & Compliance | ุชุณุฌูู ูู ุญุฑูุฉุ GDPRุ Export |
ุงูุจุฑูู ุจุช ุงูููุฏุณู ุงูุดุงู ู โ ู ูุตุฉ “ุณูุฉ ุจุฑูุฏุฌ” (Salla Bridge Platform)
๐ฏ ุงููุฏู ุงูุนุงู (Master Prompt)
ุฃูุช ู ููุฏุณ ุจุฑู ุฌูุงุช ุฑุฆูุณู (Lead Software Architect) ู ุชุฎุตุต ูู WordPress Plugin Development ูWooCommerce ูSaaS Multi-Tenant Systems. ู ุทููุจ ู ูู ุชุตู ูู ูุชูููุฐ ู ูุงุตูุฉ ููุฏุณูุฉ ูุงู ูุฉ v1.0 ูู ูุตุฉ SaaS ูุณูุทุฉ (Middleware Platform) ุชุฑุจุท ู ุชุฌุฑ ุณูุฉ (Salla) ุจู ุตุงุฏุฑ ุฎุงุฑุฌูุฉ ููู ูุชุฌุงุช (AliExpress, Amazon, Noon, Shopify, Taobao, 1688, CJ Dropshipping, ูุบูุฑูุง)ุ ู ุน ูุธุงู ุงุดุชุฑุงูุงุช ู ูุญูู ูููู ุงูู ุญุชูู ุชููุงุฆููุง ุนูุฏ ุงูุชูุงุก ุงูุงุดุชุฑุงู.
ุงูู ุทููุจ: ู ู ุงูุชุตูุฑ ุฅูู ุงูุชูููุฐ โ ูุชุงุจุฉ ุงูู ูุงุตูุฉ ูุงู ูุฉ ุจุญูุซ ูู ูู ูู ุจุฑู ุฌ ุชูููุฐูุง ุญุฑูููุง.
๐ ุงููุณู 1: ุชุนุฑูู ุงูู ูุตุฉ (Platform Definition)
ุงูุงุณู
ุงูู
ูุชุฑุญ: SallaBridge โ ุฌุณุฑ ุงูู
ูุชุฌุงุช ุงูุฐูู.
ุงููุตู:
ู
ูุตุฉ SaaS ู
ุชุนุฏุฏุฉ ุงูู
ุณุชุฃุฌุฑูู (Multi-Tenant) ุชุนู
ู ูู WordPress Plugin + ุจูุงุจุฉ ููุจ ุฎุงุฑุฌูุฉุ ุชูู
ููู ุฃุตุญุงุจ ู
ุชุงุฌุฑ ุณูุฉ ู
ู:
- ุณุญุจ ุงูู ูุชุฌุงุช ูุงูุตูุฑ ูุงูุฃุณุนุงุฑ ู ู ู ูุตุงุช ุนุงูู ูุฉ (AliExpress, Amazon, Noon, Taobao, 1688, CJ, Shopifyโฆ).
- ุฅุนุงุฏุฉ ููููุชูุง ูุชุฑุฌู ุชูุง ูุชุญุณูููุง ุจุงูุฐูุงุก ุงูุงุตุทูุงุนู.
- ุฏูุนูุง ุชููุงุฆููุง ุฅูู ู ุชุฌุฑ ุณูุฉ (Products, Categories, Images, Variants).
- ุฅุฏุงุฑุฉ ุฐูู ุนุจุฑ ููุญุฉ ุชุญูู ูุงุญุฏุฉ ู ุน ูุธุงู ุจุงูุงุช ูุงุดุชุฑุงูุงุช ุดูุฑู/ุณููู.
ุงูููู ุฉ ุงูู ุถุงูุฉ:
- ุฃุชู ุชุฉ ุงูุงุณุชูุฑุงุฏ (Bulk Import) ุจู ุนุฏู ุขูุงู ุงูู ูุชุฌุงุช/ุณุงุนุฉ.
- ุชุฑุฌู ุฉ ุนุฑุจูุฉ/ุฅูุฌููุฒูุฉ ุชููุงุฆูุฉ ุนุจุฑ AI Gateway.
- ู ุฒุงู ูุฉ ุงูุฃุณุนุงุฑ ูุงูู ุฎุฒูู.
- ูุธุงู ููู (Lock): ุนูุฏ ุนุฏู ุงูุฏูุน โ ุฅุฎูุงุก/ููู ุงูู ูุชุฌุงุช ูุงูุฃูุณุงู ูุงูุชุตูููุงุช ู ู ู ุชุฌุฑ ุณูุฉ ุชููุงุฆููุง.
๐๏ธ ุงููุณู 2: ุจููุฉ ูุงุนุฏุฉ ุงูุจูุงูุงุช (Database Schema v1.0)
ุงูุชุจ ุงูุฌุฏุงูู ุจุตูุบุฉ wp_sb_* ู
ุน dbDelta():
2.1 ุงูุฌุฏุงูู ุงูุฃุณุงุณูุฉ
| ุงูุฌุฏูู | ุงูุญููู ุงูุฑุฆูุณูุฉ | ุงูุนูุงูุงุช |
|---|---|---|
wp_sb_tenants | id, user_id, salla_store_id, salla_access_token, salla_refresh_token, plan_id, status, trial_ends_at, created_at | โ wp_users, โ wp_sb_plans |
wp_sb_plans | id, name, slug, price_monthly, price_yearly, max_products, max_images, max_sources, features_json, is_active | โ wp_sb_entitlements |
wp_sb_subscriptions | id, tenant_id, plan_id, gateway, gateway_sub_id, status, current_period_start, current_period_end, cancel_at, created_at | โ tenants, plans |
wp_sb_entitlements | id, plan_id, feature_key, limit_value, reset_period | โ plans |
wp_sb_sources | id, tenant_id, source_type (aliexpress/amazon/noon…), api_key, api_secret, is_active, last_sync_at | โ tenants |
wp_sb_import_jobs | id, tenant_id, source_id, status, total_items, processed, failed, payload_json, started_at, finished_at | โ tenants, sources |
wp_sb_products_map | id, tenant_id, source_product_id, salla_product_id, sku, price_source, price_salla, markup_percent, image_count, locked TINYINT, synced_at | โ tenants |
wp_sb_images | id, product_map_id, source_url, salla_media_id, checksum, status | โ products_map |
wp_sb_categories_map | id, tenant_id, source_cat_id, salla_cat_id, locked | โ tenants |
wp_sb_ai_jobs | id, tenant_id, job_type (translate/rewrite/seo), input_hash, output_json, tokens_used, provider, status | โ tenants |
wp_sb_webhooks_log | id, tenant_id, event, payload, signature, status, created_at | โ tenants |
wp_sb_audit_log | id, tenant_id, actor_id, action, object_type, object_id, meta_json, ip, created_at | โ tenants |
wp_sb_lock_events | id, tenant_id, reason (expired/cancelled/suspended), locked_items_count, unlocked_at, created_at | โ tenants |
2.2 ุงูููุงุฑุณ (Indexes)
UNIQUE(tenant_id, source_product_id)ย ุนููยproducts_map.INDEX(status, current_period_end)ย ุนููยsubscriptions.INDEX(tenant_id, locked)ย ุนููยproducts_mapย ูcategories_map.
๐งฉ ุงููุณู 3: ูููู ุงููPlugin (Folder & Namespace Structure)
text
salla-bridge/ โโโ salla-bridge.php (Bootstrap + Plugin Header) โโโ composer.json (PSR-4: SallaBridge\) โโโ uninstall.php โโโ src/ โ โโโ Core/ โ โ โโโ Plugin.php โ โ โโโ Activator.php โ โ โโโ Deactivator.php โ โ โโโ Installer.php (dbDelta migrations) โ โ โโโ Container.php (DI Container) โ โโโ Admin/ โ โ โโโ Menu.php โ โ โโโ Dashboards/ โ โ โ โโโ TenantDashboard.php โ โ โ โโโ AdminDashboard.php โ โ โ โโโ BillingDashboard.php โ โ โโโ Views/ โ โโโ Api/ โ โ โโโ RestController.php (Base) โ โ โโโ AuthController.php โ โ โโโ ImportController.php โ โ โโโ SyncController.php โ โ โโโ BillingController.php โ โ โโโ LockController.php โ โโโ Salla/ โ โ โโโ Client.php (OAuth2 + REST) โ โ โโโ ProductPusher.php โ โ โโโ CategoryPusher.php โ โ โโโ MediaUploader.php โ โ โโโ Webhooks.php โ โโโ Sources/ โ โ โโโ SourceInterface.php โ โ โโโ AliExpressSource.php โ โ โโโ AmazonSource.php โ โ โโโ NoonSource.php โ โ โโโ ShopifySource.php โ โ โโโ SourceFactory.php โ โโโ AI/ โ โ โโโ Gateway.php โ โ โโโ Providers/ โ โ โ โโโ OpenAIProvider.php โ โ โ โโโ AnthropicProvider.php โ โ โ โโโ GeminiProvider.php โ โ โโโ TranslateJob.php โ โ โโโ SeoRewriteJob.php โ โโโ Billing/ โ โ โโโ SubscriptionManager.php โ โ โโโ EntitlementChecker.php โ โ โโโ WooCommerceHooks.php โ โ โโโ Gateways/ โ โ โ โโโ StripeGateway.php โ โ โ โโโ PaypalGateway.php โ โ โ โโโ MoyasarGateway.php (ุณุนูุฏู) โ โ โ โโโ TapGateway.php (ุฎููุฌู) โ โ โโโ WebhookHandler.php โ โโโ Locking/ โ โ โโโ LockManager.php โ โ โโโ SallaLocker.php (unpublish products/cats on Salla) โ โ โโโ CronGuard.php โ โโโ Roles/ โ โ โโโ RoleManager.php โ โ โโโ Capabilities.php โ โโโ Cron/ โ โ โโโ SyncPrices.php โ โ โโโ CheckSubscriptions.php (hourly) โ โ โโโ RetryFailedJobs.php โ โโโ Support/ โ โโโ Logger.php โ โโโ Crypto.php (encrypt tokens at rest) โ โโโ Helpers.php โโโ assets/ (js, css, react build) โโโ languages/ โโโ tests/
Namespace root: SallaBridge\
Text Domain: salla-bridge
๐ ุงููุณู 4: REST Endpoints (v1)
Base: /wp-json/sb/v1/
Auth: Bearer JWT + HMAC signature for Salla webhooks.
| Method | Route | Purpose | Capability |
|---|---|---|---|
| POST | /auth/salla/callback | ุงุณุชูุจุงู OAuth code | public |
| POST | /import/start | ุจุฏุก ู ูู ุฉ ุงุณุชูุฑุงุฏ | sb_start_import |
| GET | /import/{job_id}/status | ุญุงูุฉ ุงูู ูู ุฉ | sb_view_jobs |
| POST | /sync/products | ู ุฒุงู ูุฉ ู ูุชุฌุงุช | sb_sync_products |
| POST | /sync/prices | ุชุญุฏูุซ ุงูุฃุณุนุงุฑ | sb_sync_prices |
| POST | /billing/subscribe | ุฅูุดุงุก ุงุดุชุฑุงู | sb_manage_billing |
| POST | /billing/cancel | ุฅูุบุงุก ุงุดุชุฑุงู | sb_manage_billing |
| GET | /billing/status | ุญุงูุฉ ุงูุงุดุชุฑุงู ูุงููentitlements | sb_view_billing |
| POST | /lock/enforce | ุชุดุบูู ุงูููู ูุฏูููุง (admin) | sb_manage_lock |
| POST | /lock/release | ุฑูุน ุงูููู ุจุนุฏ ุงูุฏูุน | sb_manage_lock |
| POST | /ai/translate | ุชุฑุฌู ุฉ ุฏูุนุฉ | sb_use_ai |
| POST | /ai/seo | ุชุญุณูู SEO | sb_use_ai |
| GET | /dashboard/stats | ุฅุญุตุงุฆูุงุช | sb_view_dashboard |
| POST | /webhooks/salla | Webhooks ู ู ุณูุฉ | public+HMAC |
| POST | /webhooks/woo | Webhooks ู ู WooCommerce | public+HMAC |
ูู endpoint ูู
ุฑ ุนุจุฑ RestController::permission_callback() ูุชุญูู ู
ู tenant_id + EntitlementChecker.
๐ ุงููุณู 5: ุชูุงู ู ุณูุฉ (Salla Integration)
- OAuth2 Flow:ย
https://accounts.salla.sa/oauth2/authย โยtokenย โ ุชุฎุฒูู ู ูุดููุฑ ููยwp_sb_tenants. - Endpoints ุงูู
ูุณุชุฎุฏู
ุฉ ู
ู Salla API:
POST /productsย โ ุฅูุดุงุก ู ูุชุฌPUT /products/{id}ย โ ุชุญุฏูุซPOST /products/{id}/imagesPOST /categoriesPUT /products/{id}ย ู ุนยstatus: hiddenย โย ููููู
- Mapping ุงูุญููู:
source.title โ salla.namesource.description โ salla.descriptionsource.price * (1 + markup/100) โ salla.pricesource.stock โ salla.quantity
- Rate Limiting:ย 60 req/min โ Backoff + Queue.
- Retry:ย 3 ู ุญุงููุงุช (exponential: 5s, 30s, 5m).
๐ณ ุงููุณู 6: ูุธุงู ุงูุจุงูุงุช ูุงูุงุดุชุฑุงูุงุช (Plans & Entitlements)
6.1 ุงูุจุงูุงุช ุงูู ูุชุฑุญุฉ
| ุงูุจุงูุฉ | ุดูุฑู | ุณููู | ู ูุชุฌุงุช | ุตูุฑ | ู ุตุงุฏุฑ | AI tokens |
|---|---|---|---|---|---|---|
| Starter | 99 SAR | 990 SAR | 500 | 2,000 | 1 | 100K |
| Growth | 299 SAR | 2,990 SAR | 5,000 | 20,000 | 3 | 1M |
| Pro | 799 SAR | 7,990 SAR | 25,000 | 100,000 | 8 | 5M |
| Enterprise | ุชุณุนูุฑ ู ุฎุตุต | โ | Unlimited | Unlimited | Unlimited | Custom |
6.2 Entitlements Logic
php
EntitlementChecker::can($tenant_id, 'import_products', $qty)
- ููุญุต:ย
subscription.status = activeย +ยnow <= current_period_endย +ยused + qty <= limit. - ุนูุฏ ุงูุชุฌุงูุฒ โ ูุฑุฌุนย
WP_Error('quota_exceeded').
6.3 WooCommerce Hooks (ูุฃู ุงูู ูุตุฉ ููุณูุง ุชูุจูู ุนูู Woo)
woocommerce_checkout_order_processedย โ ุชูุนูู ุงุดุชุฑุงู.woocommerce_order_status_completedย โ ุชู ุฏูุฏยcurrent_period_end.woocommerce_order_status_refundedย โ ุฅูุบุงุก ููุฑู.woocommerce_subscription_status_updatedย (ุฅู ููุฌุฏ WCS).woocommerce_payment_completeย โ ุฑุจุทยgateway_sub_id.
6.4 ุฏูุฑุฉ ุญูุงุฉ ุงูุงุดุชุฑุงู
trialing โ active โ past_due (grace 3 ุฃูุงู
) โ locked โ cancelled
๐ ุงููุณู 7: ูุธุงู ุงูููู ุงูุชููุงุฆู (Auto-Lock System) โ ุงูููุจ ุงููุงุจุถ
ุงูุณููุงุฑูู:
ุงูุนู ูู ุชููู ุนู ุงูุฏูุน โ ุจุนุฏ ุงูุชูุงุก
current_period_end+ grace period โ ุชูููู ุฌู ูุน ุงูู ูุชุฌุงุช ูุงูุฃูุณุงู ูุงูุชุตูููุงุช ุงูุชู ุงุณุชูุฑุฏูุง ุนุจุฑ ุงูู ูุตุฉ ูู ู ุชุฌุฑ ุณูุฉุ ูุชุฎุชูู ู ู ุงููุงุฌูุฉ.
7.1 Cron: sb_check_subscriptions (ูู ุณุงุนุฉ)
php
foreach (subscriptions where status IN ('active','past_due')) {
if (now > current_period_end + grace_days) {
LockManager::lock($tenant_id, 'expired');
}
}
7.2 LockManager::lock()
- ุฌูุจ ููย
products_map WHERE tenant_id AND locked = 0. - ููู ู
ูุชุฌ: ุงุณุชุฏุนุงุกย
Salla API PUT /products/{id} {status: 'hidden'}. - ููุณ ุงูุดูุก ููุชุตูููุงุชย
categories_map. - ุชุณุฌูู ููย
lock_events. - ุชุญุฏูุซย
locked = 1. - ุฅุฑุณุงู ุฅูู ูู + ุฅุดุนุงุฑ Dashboard.
- ุชุณุฌูู webhookย
subscription.locked.
7.3 LockManager::release()
- ุนูุณ ุงูุนู
ููุฉย
status: 'sale'. - ููุท ุจุนุฏ ุชุฃููุฏ ุงูุฏูุน ู ู Gateway webhook.
7.4 Cron Guard
- ูู
ูุน ุฃูย
import/syncย ุฌุฏูุฏ ุฅุฐุงยlocked = 1. - REST endpoints ุชุฑุฌุนย
403 subscription_locked.
๐ค ุงููุณู 8: AI Gateway
Interface ู ูุญูุฏ:
php
interface AiProviderInterface {
public function translate(string $text, string $to, string $from = 'auto'): string;
public function rewrite(string $text, string $tone): string;
public function seo(string $title, string $desc): array;
}
- Routing:ย ูุฎุชุงุฑ Provider ุญุณุจ ุงูุชูููุฉ ูุงููุบุฉ (OpenAI ููุนุฑุจูุฉุ Anthropic ูููSEOุ Gemini ููุชุฑุฌู ุฉ ุงูุฌู ุงุนูุฉ).
- Caching:ย
input_hashย โ ูู ูุน ุฅุนุงุฏุฉ ุงูุฏูุน ูููุณ ุงููุต. - Rate Limit per tenant:ย ุญุณุจ ุงูุจุงูุฉ.
- Fallback:ย ุฅุฐุง ูุดู OpenAI โ Anthropic โ Gemini.
๐ฅ ุงููุณู 9: Roles & Capabilities
| Role | Capabilities |
|---|---|
sb_tenant_owner | ูู ุตูุงุญูุงุช ุงูู ุณุชุฃุฌุฑ |
sb_tenant_manager | import, sync, view (ุจุฏูู billing) |
sb_tenant_viewer | view ููุท |
sb_support | view_all_tenants, impersonate |
sb_platform_admin | ูู ุดูุก + manage_lock, manage_plans |
php
add_role('sb_tenant_owner', 'SallaBridge Tenant', [
'sb_start_import' => true,
'sb_sync_products' => true,
'sb_manage_billing' => true,
'sb_manage_lock' => false, // admin only
'sb_use_ai' => true,
'sb_view_dashboard' => true,
]);
๐ ุงููุณู 10: Dashboards
10.1 Tenant Dashboard (React SPA)
- ุจุทุงูุงุช: ู ูุชุฌุงุช ู ุณุชูุฑุฏุฉ | ุตูุฑ | AI tokens ู ุณุชุฎุฏู ุฉ | ุญุงูุฉ ุงูุงุดุชุฑุงู (ุนุฏูุงุฏ ุชูุงุฒูู).
- ุฑุณู ุจูุงูู: imports/day.
- ุฌุฏูู: ุขุฎุฑ 20 ู ูู ุฉ ุงุณุชูุฑุงุฏ + ุญุงูุชูุง.
- ุชุญุฐูุฑ ุฃุญู
ุฑ ุฅุฐุงย
current_period_end - now < 7 days.
10.2 Admin Dashboard
- ุนุฏุฏ ุงูู ุณุชุฃุฌุฑููุ MRRุ Churnุ Locked Tenants.
- ุฒุฑย
Force Lock / Release. - ุณุฌูย
audit_log.
10.3 Billing Dashboard
- Upgrade/Downgradeุ ุงูููุงุชูุฑุ ุทุฑููุฉ ุงูุฏูุนุ Cancel.
โฑ๏ธ ุงููุณู 11: ุฎุทุฉ ุชูููุฐ ุฃูู 60 ุฏูููุฉ (Minute-by-Minute)
| ุงูุฏูููุฉ | ุงูู ูู | ุงูู ุฎุฑุฌ |
|---|---|---|
| 0โ5 | salla-bridge.php | Plugin Header + Autoload + Activation Hook |
| 5โ10 | src/Core/Installer.php | dbDelta ููู ุงูุฌุฏุงูู |
| 10โ15 | src/Core/Plugin.php + Container.php | Bootstrap + DI |
| 15โ20 | src/Roles/RoleManager.php + Capabilities.php | ุชุณุฌูู ุงูุฃุฏูุงุฑ |
| 20โ25 | src/Api/RestController.php | Base REST + namespace |
| 25โ30 | src/Api/AuthController.php | OAuth callback |
| 30โ35 | src/Salla/Client.php | OAuth2 + request wrapper |
| 35โ40 | src/Sources/SourceInterface.php + SourceFactory.php | ูุงุฌูุฉ ุงูู ุตุงุฏุฑ |
| 40โ45 | src/Sources/AliExpressSource.php | ุฃูู ู ุตุฏุฑ ูุนูู |
| 45โ50 | src/Billing/SubscriptionManager.php + EntitlementChecker.php | ู ูุทู ุงูุจุงูุงุช |
| 50โ55 | src/Locking/LockManager.php + SallaLocker.php | ูุธุงู ุงูููู |
| 55โ60 | src/Cron/CheckSubscriptions.php + ุชุณุฌููู | Cron ูู ุณุงุนุฉ |
ุงููุงุชุฌ ุจุนุฏ 60 ุฏูููุฉ: Plugin ูููุนููุ ููุดุฆ ุงูุฌุฏุงููุ ูุณุฌูู RESTุ ูุฑุจุท ุณูุฉ OAuthุ ูุณุชูุฑุฏ ู ู AliExpressุ ูุฏูุฑ ุงุดุชุฑุงูุ ููููู ุชููุงุฆููุง ุนูุฏ ุงูุงูุชูุงุก.
๐งช ุงููุณู 12: ู ุนุงููุฑ ุงููุจูู (Acceptance Criteria)
- โ ุชูุนูู Plugin ูููุดุฆ 13 ุฌุฏูููุง ุจุฏูู ุฃุฎุทุงุก.
- โ OAuth ู ุน ุณูุฉ ูุนู ู ููุฎุฒูู tokens ู ูุดููุฑุฉ.
- โ ุงุณุชูุฑุงุฏ 100 ู ูุชุฌ ู ู AliExpress ูู < 5 ุฏูุงุฆู.
- โ ู ูุชุฌุงุช ุชุธูุฑ ูู ุณูุฉ ุจุงูุตูุฑ ูุงูุชุตูููุงุช.
- โ
ุงุดุชุฑุงู ุดูุฑู ุนุจุฑ Moyasar ูููุดุฆย
subscription.active. - โ
ุนูุฏ ุงูุชูุงุกย
current_period_end + graceย โ ูู ุงูู ูุชุฌุงุชยhiddenย ูู ุณูุฉ ุฎูุงู ุณุงุนุฉ. - โ
ุนูุฏ ุชุฌุฏูุฏ ุงูุฏูุน โย
release()ย ูุฑุฌูุน ูู ุดูุก. - โ Dashboard ูุนุฑุถ ุงูุนุฏูุงุฏ ูุงูุฅุญุตุงุฆูุงุช.
- โ REST endpoints ู ุญู ูุฉ ุจูCapabilities.
- โ Audit log ูุณุฌูู ูู ุนู ููุฉ ุญุณุงุณุฉ.
๐ฆ ุงููุณู 13: ู ุฎุฑุฌุงุช ู ุทููุจุฉ ู ูู (Deliverables)
ุงูุชุจ ุงูุขูุ ุจุงูุชุฑุชูุจ:
- ููุฏ SQL ูุงู ูย ููู ุงูุฌุฏุงูู (MySQL 8, utf8mb4).
- ููุฏ PHP ูุงู ูย ููู ููุงุช ูู ุฎุทุฉ ุงูู60 ุฏูููุฉ (ูุง pseudo-code).
- ู ุฎุทุท ERD ูุตูย ููุนูุงูุงุช.
- OpenAPI 3.0 YAMLย ููู REST endpoints.
- Schema ูููPayloadsย (Salla webhook, Woo webhook, AI job).
- Pseudo-codeย ููCronย
CheckSubscriptionsย ูLockManager::lock/release. - ุฎุทุฉ ุงุฎุชุจุงุฑย (Unit + Integration + E2E).
- Roadmapย ููv1.1 (Multi-store, WhatsApp notifications, Mobile app).
๐ซ ูููุฏ ุตุงุฑู ุฉ
- ูุง ุชุณุชุฎุฏู
ย
eval, ูุงยbase64ย ููููุฏ. - ูู ุงููSQL ุนุจุฑย
$wpdb->prepare. - ูู ุงููTokens ู
ูุดููุฑุฉ ุจู
Crypto::encrypt()ย (Sodium). - ุงูุงูุชุฒุงู ุจูWordPress Coding Standards + PSR-12.
- ุงููMulti-Tenancy ุนุจุฑย
tenant_idย ูู ูู ุฌุฏูู (Row-level isolation). - ูุง ุชุญุฐู ุจูุงูุงุช ุนูุฏ ุงูุฅูุบุงุก โ ููุทย
locked = 1.
ุงุจุฏุฃ ุงูุขู ุจูุชุงุจุฉ ุงูู ูุงุตูุฉ ุงูููุฏุณูุฉ ุงููุงู ูุฉ v1.0ุ ู ูููุง ุจู ููุ ุณุทุฑูุง ุจุณุทุฑุ ุฌุงูุฒุฉ ูููุณุฎ ุฅูู IDE.
๐ ุงูู ูุญู ุงูููุฏุณู ุงูู ูุณูุน โ SallaBridge v1.1
ุงุณุชูู ุงููุง ููู ูุงุตูุฉ v1.0ุ ูุฐุง ุงูู ูุญู ูุบุทู ุงูุทุจูุงุช ุงูุนู ููุฉ: ุงูุชุดููุฑุ ุงูุทูุงุจูุฑุ ุงูุฃู ุงูุ ุงูุชูุณุนุ ุงูุงุฎุชุจุงุฑุงุชุ DevOpsุ ูุงูุชูุงุตูู ุงูุชูููุฐูุฉ ุงููุงู ูุฉ.
๐ ุงููุณู 14: ุทุจูุฉ ุงูุชุดููุฑ ูุงูุฃู ุงู (Crypto & Security)
14.1 src/Support/Crypto.php
php
<?php
declare(strict_types=1);
namespace SallaBridge\Support;
final class Crypto {
private const CIPHER = 'sodium_crypto_secretbox';
private static string $key;
public static function boot(): void {
$raw = defined('SB_ENCRYPTION_KEY') ? SB_ENCRYPTION_KEY : get_option('sb_encryption_key');
if (!$raw) {
$raw = base64_encode(random_bytes(32));
update_option('sb_encryption_key', $raw, false);
}
self::$key = base64_decode($raw);
}
public static function encrypt(string $plain): string {
$nonce = random_bytes(SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = sodium_crypto_secretbox($plain, $nonce, self::$key);
return base64_encode($nonce . $cipher);
}
public static function decrypt(string $encoded): string {
$decoded = base64_decode($encoded, true);
if ($decoded === false) return '';
$nonce = substr($decoded, 0, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$cipher = substr($decoded, SODIUM_CRYPTO_SECRETBOX_NONCEBYTES);
$plain = sodium_crypto_secretbox_open($cipher, $nonce, self::$key);
return $plain === false ? '' : $plain;
}
}
14.2 ููุงุนุฏ ุงูุฃู ุงู ุงูุฅูุฒุงู ูุฉ
| ุงูุจูุฏ | ุงูุชูููุฐ |
|---|---|
| Tokens at Rest | Sodium secretbox (ุฃุนูุงู) |
| HTTPS Only | force_ssl_admin() + HSTS header |
| REST Auth | JWT + HMAC-SHA256 ููู webhooks |
| SQL Injection | $wpdb->prepare ุฏุงุฆู
ูุง |
| XSS | esc_html, esc_attr, wp_kses_post |
| CSRF | wp_nonce ููู form |
| Rate Limit | Transient-based per IP + tenant |
| File Uploads | ูุญุต MIME + wp_check_filetype_and_ext |
| Secrets | ูุง ุชูุฎุฒูู ูู Git โ .env + wp-config.php |
| Audit | ูู ุนู
ููุฉ ูุชุงุจุฉ โ audit_log |
14.3 HMAC Webhook Verification
php
public static function verify_salla_webhook(string $raw, string $signature, string $secret): bool {
$expected = base64_encode(hash_hmac('sha256', $raw, $secret, true));
return hash_equals($expected, $signature);
}
โ๏ธ ุงููุณู 15: ูุธุงู ุงูุทูุงุจูุฑ (Job Queue)
15.1 ูู
ุงุฐุง ูุง ูุนุชู
ุฏ ุนูู wp_cron ููุทุ
wp_cronย ูุนุชู ุฏ ุนูู ุฒูุงุฑุงุช ุงูู ุณุชุฎุฏู โ ุบูุฑ ู ูุซูู ูููSaaS.- ุงูุญู:ย Action Schedulerย (ู ุฏู ุฌ ู ุน WooCommerce) + Queue ู ุฎุตูุต.
15.2 ุจููุฉ Queue
php
namespace SallaBridge\Core;
final class Queue {
public const HOOK = 'sb_process_job';
public static function push(string $job_type, array $payload, int $priority = 10, int $delay = 0): int {
return as_schedule_single_action(
time() + $delay,
self::HOOK,
['type' => $job_type, 'payload' => $payload],
'salla-bridge',
$priority
);
}
public static function register(): void {
add_action(self::HOOK, [self::class, 'handle'], 10, 2);
}
public static function handle(string $type, array $payload): void {
$handler = JobRegistry::resolve($type);
if (!$handler) return;
try {
$handler->handle($payload);
} catch (\Throwable $e) {
Logger::error("Job failed: {$type}", ['error' => $e->getMessage()]);
$attempts = ($payload['_attempts'] ?? 0) + 1;
if ($attempts < 3) {
$payload['_attempts'] = $attempts;
self::push($type, $payload, 10, 30 * $attempts);
} else {
self::move_to_dlq($type, $payload, $e);
}
}
}
}
15.3 ุฃููุงุน ุงููJobs
| Job Type | Handler | Batch |
|---|---|---|
import.product | ImportProductJob | 1 ู ูุชุฌ |
import.batch | ImportBatchJob | 50 ู ูุชุฌ |
sync.price | SyncPriceJob | 100 ู ูุชุฌ |
upload.image | UploadImageJob | 1 ุตูุฑุฉ |
ai.translate | TranslateJob | 20 ูุต |
lock.tenant | LockTenantJob | tenant ูุงู ู |
release.tenant | ReleaseTenantJob | tenant ูุงู ู |
notify.email | EmailJob | 1 |
15.4 Dead Letter Queue (DLQ)
ุฌุฏูู wp_sb_dlq (id, job_type, payload, error, failed_at) + Dashboard ูุนุฑุถูุง ูุฅุนุงุฏุฉ ุชุดุบูููุง.
๐๏ธ ุงููุณู 16: Multi-Tenancy ุงูุนู ูู
16.1 Row-Level Isolation
- ูุงุนุฏุฉ ุฐูุจูุฉ:ย ูู ุฌุฏูู (ุนุฏุงย
plansย ูentitlements) ูุญู ูยtenant_id BIGINT UNSIGNED NOT NULL. - ูู Query ูู
ุฑ ุนุจุฑย
TenantScope::apply($sql).
php
final class TenantScope {
public static function id(): int {
return (int) (Container::get('current_tenant')?->id ?? 0);
}
public static function where(string $table): string {
global $wpdb;
return $wpdb->prepare("{$table}.tenant_id = %d", self::id());
}
}
16.2 Tenant Context Resolution
text
Request โ Middleware:
1. JWT decode โ user_id
2. users โ tenants (1:1 ุฃู 1:N ู
ุน owner)
3. ุชุญู
ูู Tenant object + Subscription + Entitlements
4. Container::set('current_tenant', $tenant)
5. ุงูุชุญูู ู
ู Lock status โ ุฑูุถ ุฅู ู
ูุนูู
16.3 Tenant Lifecycle
text
registered โ trial (14 ููู
) โ active โ past_due โ locked โ cancelled
โ โ
โโโ soft delete โโโโ
๐ ุงููุณู 17: ู ุญูููุงุช ุงูู ุตุงุฏุฑ (Source Adapters) ุจุงูุชูุตูู
17.1 ุงููุงุฌูุฉ ุงูู ูุญุฏุฉ
php
interface SourceInterface {
public function authenticate(array $credentials): bool;
public function search(string $query, int $page = 1): array;
public function fetch_product(string $external_id): array; // DTO ู
ูุญูุฏ
public function fetch_images(string $external_id): array;
public function fetch_categories(): array;
public function normalize(array $raw): ProductDTO;
}
17.2 ProductDTO ุงูู
ูุญูุฏ
php
final class ProductDTO {
public string $external_id;
public string $title;
public string $description;
public float $price;
public string $currency;
public int $stock;
public string $sku;
public array $images = []; // ['url' => '', 'alt' => '']
public array $variants = []; // ['name'=>'', 'price'=>0, 'stock'=>0]
public string $category_path; // "Electronics > Phones"
public array $attributes = []; // ['color'=>'red']
public array $raw = []; // ุงูุฃุตู ููุฑุฌูุน
}
17.3 ุฌุฏูู ุงูู ุตุงุฏุฑ ุงูู ุฏุนูู ุฉ
| ุงูู ุตุฏุฑ | Auth | Rate Limit | ู ูุงุญุธุงุช |
|---|---|---|---|
| AliExpress | AppKey + AppSecret + OAuth | 100/min | ุนุจุฑ aliexpress.open.api |
| Amazon PA-API | AccessKey + Secret + PartnerTag | 1 req/s | ูุญุชุงุฌ Associate |
| Noon | Partner API | 60/min | ูุญุชุงุฌ ุงุชูุงู ุชุฌุงุฑู |
| Shopify | Store URL + Admin Token | 2 req/s | REST Admin API |
| Taobao | AppKey + Session | 60/min | ูุญุชุงุฌ ูุณูุท ุตููู |
| 1688 | AppKey + Session | 60/min | ู ู ุงุซู Taobao |
| CJ Dropshipping | API Key | 120/min | ุงูุฃุณูู ููุจุฏุงูุฉ |
| Alibaba | OAuth | 30/min | ููุฌู ูุฉ |
17.4 ุฎูุงุฑุฒู ูุฉ ุงุฎุชูุงุฑ ุงูู ุตุฏุฑ
php
SourceFactory::make($type, $tenant) // ูุชุญูู ู ู: - ุงูู ุตุฏุฑ ู ุฏุนูู ูู ุงูุจุงูุฉ (max_sources) - credentials ุตุญูุญุฉ + ุบูุฑ ู ูุชููุฉ - rate limit ู ุชุงุญ - circuit breaker ู ุบูู
๐ค ุงููุณู 18: AI Gateway ุงูู ุชูุฏู
18.1 Routing Strategy (Cost-Aware)
php
final class Gateway {
public function translate(string $text, string $to): string {
$provider = $this->pick_provider('translate', strlen($text), $to);
return $provider->translate($text, $to);
}
private function pick_provider(string $task, int $len, string $lang): AiProviderInterface {
// ููุงุนุฏ:
// - ูุตูุต <500 char + ุนุฑุจู โ Gemini Flash (ุฑุฎูุต)
// - ูุตูุต >2000 char โ OpenAI GPT-4o-mini
// - SEO/rewrite โ Claude Sonnet
// - fallback chain ุนูุฏ ุงููุดู
}
}
18.2 AI Caching Layer
ุฌุฏูู wp_sb_ai_cache (hash, provider, output, tokens, created_at) โ ูู
ูุน ุฅุนุงุฏุฉ ุงูุฏูุน ูููุณ ุงูู
ุฏุฎู.
18.3 Prompt Templates
php
final class Prompts {
public const TRANSLATE = <<<PROMPT
Translate the following e-commerce product text to %s.
Preserve: brand names, units, numbers, HTML tags.
Tone: persuasive, marketing.
Input: %s
Output (JSON): {"title":"","description":"","tags":[]}
PROMPT;
public const SEO = <<<PROMPT
Rewrite as SEO-optimized Arabic product listing.
Include: primary keyword, 3 long-tail, meta description (155 char).
Input: %s
PROMPT;
}
18.4 ุญุฏูุฏ AI ุญุณุจ ุงูุจุงูุฉ
| ุงูุจุงูุฉ | Tokens/ุดูุฑ | Rate/min | Providers |
|---|---|---|---|
| Starter | 100K | 10 | Gemini |
| Growth | 1M | 30 | + OpenAI |
| Pro | 5M | 100 | + Anthropic |
| Enterprise | Custom | Custom | All + Dedicated |
๐ฐ ุงููุณู 19: ุงูููุชุฑุฉ ูุงูุงุดุชุฑุงูุงุช (Billing Deep Dive)
19.1 ุฏูุฑุฉ ุญูุงุฉ ุงูุงุดุชุฑุงู (State Machine)
text
โโโโโโโโโโโโ
โ trialing โ
โโโโโโฌโโโโโโ
โ (14 ููู
)
โผ
โโโโโโโโโโโโ ูุดู ุฏูุน โโโโโโโโโโโโโ
โ active โโโโโโโโโโโโโโบโ past_due โ
โโโโโโฌโโโโโโ โโโโโโโฌโโโโโโ
โ ุชุฌุฏูุฏ โ (3 ุฃูุงู
)
โ โผ
โ โโโโโโโโโโโโ
โ โ locked โ
โ โโโโโโฌโโโโโโ
โ โ ุฏูุน
โโโโโโโโโโโโโโโโโโโโโโโโโโโ
โ ุฅูุบุงุก
โผ
โโโโโโโโโโโโโ
โ cancelled โ
โโโโโโโโโโโโโ
19.2 SubscriptionManager::renew()
php
public function renew(int $subscription_id, string $gateway_txn): bool {
$sub = $this->repo->find($subscription_id);
$plan = $this->plans->find($sub->plan_id);
$sub->current_period_start = time();
$sub->current_period_end = $sub->billing_cycle === 'yearly'
? strtotime('+1 year')
: strtotime('+1 month');
$sub->status = 'active';
$sub->gateway_sub_id = $gateway_txn;
$this->repo->save($sub);
$this->entitlements->reset_usage($sub->tenant_id, $sub->current_period_start);
$this->events->dispatch('subscription.renewed', $sub);
// ุฑูุน ุงูููู ุฅู ููุฌุฏ
if ($this->lock->is_locked($sub->tenant_id)) {
$this->lock->release($sub->tenant_id, 'renewal');
}
return true;
}
19.3 Webhook Handlers ููู Gateway
| Gateway | ุญุฏุซ ุงูุฏูุน | ุญุฏุซ ุงูุฅูุบุงุก | ุงูุชูููุน |
|---|---|---|---|
| Stripe | invoice.paid | customer.subscription.deleted | Stripe-Signature |
| PayPal | BILLING.SUBSCRIPTION.ACTIVATED | ...CANCELLED | PAYPAL-TRANSMISSION-SIG |
| Moyasar | payment_paid | subscription_canceled | HMAC-SHA256 |
| Tap | charge.succeeded | subscription.cancelled | hashstring |
19.4 ุงูููุงุชูุฑ (Invoices)
- ุชูููุฏ PDF ุนุจุฑย
dompdfย ุฃูยTCPDF. - ุชุฎุฒูู ููย
wp-content/uploads/sb-invoices/{tenant_id}/. - ุฅุฑุณุงู ุชููุงุฆู ุจุงูุจุฑูุฏ + ุฑุงุจุท ุฏุงุฆู .
๐ ุงููุณู 20: ูุธุงู ุงูููู โ ุงูุชูููุฐ ุงููุงู ู
20.1 LockManager.php (ูุงู
ู)
php
<?php
declare(strict_types=1);
namespace SallaBridge\Locking;
final class LockManager {
public function __construct(
private \SallaBridge\Salla\SallaLocker $locker,
private \SallaBridge\Core\Repositories\ProductsMapRepo $products,
private \SallaBridge\Core\Repositories\CategoriesMapRepo $categories,
private \SallaBridge\Core\Events\Dispatcher $events,
private \SallaBridge\Support\Logger $logger
) {}
public function lock(int $tenant_id, string $reason): int {
$this->logger->info("Locking tenant {$tenant_id}", ['reason' => $reason]);
$product_ids = $this->products->get_unlocked_ids($tenant_id);
$cat_ids = $this->categories->get_unlocked_ids($tenant_id);
$total = 0;
foreach (array_chunk($product_ids, 50) as $chunk) {
$total += $this->locker->hide_products($tenant_id, $chunk);
$this->products->mark_locked($tenant_id, $chunk);
usleep(500_000); // 0.5s ูููrate limit
}
foreach ($cat_ids as $cat_id) {
$this->locker->hide_category($tenant_id, $cat_id);
$this->categories->mark_locked($tenant_id, $cat_id);
}
$this->events->dispatch('tenant.locked', [
'tenant_id' => $tenant_id,
'reason' => $reason,
'products' => count($product_ids),
'categories'=> count($cat_ids),
]);
$this->log_lock_event($tenant_id, $reason, $total);
return $total;
}
public function release(int $tenant_id, string $trigger): int {
$this->logger->info("Releasing tenant {$tenant_id}", ['trigger' => $trigger]);
$product_ids = $this->products->get_locked_ids($tenant_id);
$cat_ids = $this->categories->get_locked_ids($tenant_id);
$total = 0;
foreach (array_chunk($product_ids, 50) as $chunk) {
$total += $this->locker->publish_products($tenant_id, $chunk);
$this->products->mark_unlocked($tenant_id, $chunk);
usleep(500_000);
}
foreach ($cat_ids as $cat_id) {
$this->locker->publish_category($tenant_id, $cat_id);
$this->categories->mark_unlocked($tenant_id, $cat_id);
}
$this->events->dispatch('tenant.released', [
'tenant_id' => $tenant_id,
'trigger' => $trigger,
'products' => count($product_ids),
]);
return $total;
}
public function is_locked(int $tenant_id): bool {
return (bool) get_transient("sb_locked_{$tenant_id}");
}
}
20.2 SallaLocker.php
php
final class SallaLocker {
public function __construct(private \SallaBridge\Salla\Client $client) {}
public function hide_products(int $tenant_id, array $salla_product_ids): int {
$count = 0;
foreach ($salla_product_ids as $pid) {
$res = $this->client->for_tenant($tenant_id)
->put("/products/{$pid}", ['status' => 'hidden']);
if (!is_wp_error($res)) $count++;
}
return $count;
}
public function publish_products(int $tenant_id, array $ids): int {
$count = 0;
foreach ($ids as $pid) {
$res = $this->client->for_tenant($tenant_id)
->put("/products/{$pid}", ['status' => 'sale']);
if (!is_wp_error($res)) $count++;
}
return $count;
}
public function hide_category(int $tenant_id, int $cat_id): bool {
$res = $this->client->for_tenant($tenant_id)
->put("/categories/{$cat_id}", ['status' => 'hidden']);
return !is_wp_error($res);
}
public function publish_category(int $tenant_id, int $cat_id): bool {
$res = $this->client->for_tenant($tenant_id)
->put("/categories/{$cat_id}", ['status' => 'active']);
return !is_wp_error($res);
}
}
20.3 Cron: CheckSubscriptions
php
final class CheckSubscriptions {
public const HOOK = 'sb_hourly_check';
public static function register(): void {
if (!wp_next_scheduled(self::HOOK)) {
wp_schedule_event(time(), 'hourly', self::HOOK);
}
add_action(self::HOOK, [self::class, 'run']);
}
public static function run(): void {
global $wpdb;
$grace_days = (int) get_option('sb_grace_days', 3);
$now = time();
$threshold = $now - ($grace_days * DAY_IN_SECONDS);
$subs = $wpdb->get_results($wpdb->prepare("
SELECT id, tenant_id, status, current_period_end
FROM {$wpdb->prefix}sb_subscriptions
WHERE status IN ('active','past_due')
AND current_period_end < %d
", $threshold));
$lock = Container::get(LockManager::class);
foreach ($subs as $sub) {
// 1) ุญุงูู ุชุญุตูู ุงูุฏูุน ุฃููุงู (Retry)
if ($sub->status === 'active') {
$wpdb->update("{$wpdb->prefix}sb_subscriptions",
['status' => 'past_due'], ['id' => $sub->id]);
continue;
}
// 2) past_due + ุงูุชูุช ุงูู
ููุฉ โ ุงููู
if ($sub->status === 'past_due') {
$lock->lock((int) $sub->tenant_id, 'expired');
$wpdb->update("{$wpdb->prefix}sb_subscriptions",
['status' => 'locked'], ['id' => $sub->id]);
self::notify_tenant($sub->tenant_id);
}
}
}
private static function notify_tenant(int $tenant_id): void {
$email = get_userdata(get_user_by('id', $tenant_id)->user_email ?? 0);
wp_mail(
$email?->user_email ?? get_option('admin_email'),
'ุชู
ุฅููุงู ู
ุชุฌุฑู ู
ุคูุชูุง',
'ุงูุชูู ุงุดุชุฑุงูู ูู SallaBridge. ุฌุฏูุฏ ุงูุขู ูุฅุนุงุฏุฉ ุชูุนูู ู
ูุชุฌุงุชู.'
);
}
}
๐ ุงููุณู 21: ุชูุงู ู ุณูุฉ (Salla) โ ุงูุทุจูุฉ ุงููุงู ูุฉ
21.1 Salla\Client.php
php
final class Client {
private const API_BASE = 'https://api.salla.dev/admin/v2';
private const OAUTH_AUTH = 'https://accounts.salla.sa/oauth2/auth';
private const OAUTH_TOKEN = 'https://accounts.salla.sa/oauth2/token';
private ?int $tenant_id = null;
public function for_tenant(int $tenant_id): self {
$clone = clone $this;
$clone->tenant_id = $tenant_id;
return $clone;
}
private function token(): string {
$tenant = Container::get(TenantRepo::class)->find($this->tenant_id);
$token = Crypto::decrypt($tenant->salla_access_token);
// ุชุญูู ู
ู ุงูุงูุชูุงุก
if ($tenant->salla_expires_at < time() + 300) {
$token = $this->refresh($tenant);
}
return $token;
}
private function refresh(object $tenant): string {
$res = wp_remote_post(self::OAUTH_TOKEN, [
'body' => [
'grant_type' => 'refresh_token',
'refresh_token' => Crypto::decrypt($tenant->salla_refresh_token),
'client_id' => SB_SALLA_CLIENT_ID,
'client_secret' => SB_SALLA_CLIENT_SECRET,
],
]);
$body = json_decode(wp_remote_retrieve_body($res), true);
if (!empty($body['access_token'])) {
Container::get(TenantRepo::class)->update_tokens($tenant->id, $body);
return $body['access_token'];
}
throw new \RuntimeException('Salla token refresh failed');
}
public function get(string $endpoint, array $query = []): array|\WP_Error {
return $this->request('GET', $endpoint, $query);
}
public function post(string $endpoint, array $body): array|\WP_Error {
return $this->request('POST', $endpoint, $body);
}
public function put(string $endpoint, array $body): array|\WP_Error {
return $this->request('PUT', $endpoint, $body);
}
private function request(string $method, string $endpoint, array $data): array|\WP_Error {
$args = [
'method' => $method,
'headers' => [
'Authorization' => 'Bearer ' . $this->token(),
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'timeout' => 30,
];
if ($method === 'GET') {
$url = add_query_arg($data, self::API_BASE . $endpoint);
} else {
$url = self::API_BASE . $endpoint;
$args['body'] = wp_json_encode($data);
}
$res = wp_remote_request($url, $args);
if (is_wp_error($res)) return $res;
$code = wp_remote_retrieve_response_code($res);
$body = json_decode(wp_remote_retrieve_body($res), true);
if ($code >= 400) {
return new \WP_Error("salla_http_{$code}", $body['message'] ?? 'Salla error', $body);
}
return $body ?? [];
}
}
21.2 Rate Limiter ูุณูุฉ
php
final class SallaRateLimiter {
private const MAX_PER_MIN = 60;
public static function acquire(int $tenant_id): void {
$key = "sb_salla_rl_{$tenant_id}";
$count = (int) get_transient($key);
if ($count >= self::MAX_PER_MIN) {
sleep(2); // backoff
$count = 0;
}
set_transient($key, $count + 1, MINUTE_IN_SECONDS);
}
}
21.3 Webhooks ู ู ุณูุฉ
| ุงูุญุฏุซ | ุงูุงุณุชุฌุงุจุฉ |
|---|---|
order.created | ู ุฒุงู ูุฉ ุงูู ุฎุฒูู |
product.updated | ูุดู ุงูุชุถุงุฑุจ |
app.store.authorize | ุชุฎุฒูู tokens |
app.uninstalled | soft delete ูููtenant |
๐ ุงููุณู 22: ูุงุนุฏุฉ ุงูุจูุงูุงุช โ SQL ุงููุงู ู
sql
-- 1. ุงูุฎุทุท
CREATE TABLE {$prefix}sb_plans (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
name VARCHAR(100) NOT NULL,
slug VARCHAR(50) NOT NULL,
price_monthly DECIMAL(10,2) NOT NULL DEFAULT 0,
price_yearly DECIMAL(10,2) NOT NULL DEFAULT 0,
currency CHAR(3) NOT NULL DEFAULT 'SAR',
max_products INT UNSIGNED NOT NULL DEFAULT 500,
max_images INT UNSIGNED NOT NULL DEFAULT 2000,
max_sources TINYINT UNSIGNED NOT NULL DEFAULT 1,
max_ai_tokens INT UNSIGNED NOT NULL DEFAULT 100000,
features_json JSON NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
sort_order SMALLINT NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_slug (slug),
KEY idx_active_sort (is_active, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 2. ุงูู
ุณุชุฃุฌุฑูู
CREATE TABLE {$prefix}sb_tenants (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
user_id BIGINT UNSIGNED NOT NULL,
store_name VARCHAR(200) NOT NULL,
salla_store_id VARCHAR(64) NULL,
salla_access_token TEXT NULL,
salla_refresh_token TEXT NULL,
salla_expires_at DATETIME NULL,
plan_id BIGINT UNSIGNED NULL,
status ENUM('trial','active','past_due','locked','cancelled') NOT NULL DEFAULT 'trial',
trial_ends_at DATETIME NULL,
locale CHAR(5) NOT NULL DEFAULT 'ar',
timezone VARCHAR(64) NOT NULL DEFAULT 'Asia/Riyadh',
meta_json JSON NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_user (user_id),
KEY idx_status (status),
KEY idx_plan (plan_id),
CONSTRAINT fk_tenant_plan FOREIGN KEY (plan_id) REFERENCES {$prefix}sb_plans(id) ON DELETE SET NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 3. ุงูุงุดุชุฑุงูุงุช
CREATE TABLE {$prefix}sb_subscriptions (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
plan_id BIGINT UNSIGNED NOT NULL,
billing_cycle ENUM('monthly','yearly') NOT NULL DEFAULT 'monthly',
gateway VARCHAR(32) NOT NULL,
gateway_sub_id VARCHAR(128) NULL,
gateway_customer_id VARCHAR(128) NULL,
status ENUM('trialing','active','past_due','locked','cancelled') NOT NULL DEFAULT 'trialing',
amount DECIMAL(10,2) NOT NULL DEFAULT 0,
currency CHAR(3) NOT NULL DEFAULT 'SAR',
current_period_start DATETIME NULL,
current_period_end DATETIME NULL,
cancel_at DATETIME NULL,
cancelled_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_tenant_status (tenant_id, status),
KEY idx_period_end (status, current_period_end),
CONSTRAINT fk_sub_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_sub_plan FOREIGN KEY (plan_id) REFERENCES {$prefix}sb_plans(id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 4. ุงูู
ุตุงุฏุฑ
CREATE TABLE {$prefix}sb_sources (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
source_type VARCHAR(32) NOT NULL,
label VARCHAR(100) NOT NULL,
credentials TEXT NULL,
config_json JSON NULL,
is_active TINYINT(1) NOT NULL DEFAULT 1,
last_sync_at DATETIME NULL,
last_error TEXT NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_tenant_type (tenant_id, source_type),
CONSTRAINT fk_source_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 5. ุฎุฑูุทุฉ ุงูู
ูุชุฌุงุช
CREATE TABLE {$prefix}sb_products_map (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
source_id BIGINT UNSIGNED NOT NULL,
source_product_id VARCHAR(128) NOT NULL,
salla_product_id VARCHAR(64) NULL,
sku VARCHAR(128) NULL,
title VARCHAR(500) NULL,
price_source DECIMAL(12,2) NOT NULL DEFAULT 0,
price_salla DECIMAL(12,2) NOT NULL DEFAULT 0,
markup_percent DECIMAL(5,2) NOT NULL DEFAULT 0,
currency CHAR(3) NOT NULL DEFAULT 'SAR',
image_count SMALLINT UNSIGNED NOT NULL DEFAULT 0,
stock INT NOT NULL DEFAULT 0,
locked TINYINT(1) NOT NULL DEFAULT 0,
sync_status ENUM('pending','synced','failed','outdated') NOT NULL DEFAULT 'pending',
last_error TEXT NULL,
synced_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_source (tenant_id, source_id, source_product_id),
KEY idx_salla (tenant_id, salla_product_id),
KEY idx_locked (tenant_id, locked),
CONSTRAINT fk_pm_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE,
CONSTRAINT fk_pm_source FOREIGN KEY (source_id) REFERENCES {$prefix}sb_sources(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 6. ุงูุตูุฑ
CREATE TABLE {$prefix}sb_images (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
product_map_id BIGINT UNSIGNED NOT NULL,
source_url VARCHAR(1000) NOT NULL,
salla_media_id VARCHAR(64) NULL,
checksum CHAR(64) NULL,
position SMALLINT UNSIGNED NOT NULL DEFAULT 0,
status ENUM('pending','uploaded','failed') NOT NULL DEFAULT 'pending',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_pm (product_map_id),
KEY idx_status (status),
CONSTRAINT fk_img_pm FOREIGN KEY (product_map_id) REFERENCES {$prefix}sb_products_map(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 7. ุฎุฑูุทุฉ ุงูุชุตูููุงุช
CREATE TABLE {$prefix}sb_categories_map (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
source_id BIGINT UNSIGNED NOT NULL,
source_cat_id VARCHAR(128) NOT NULL,
salla_cat_id VARCHAR(64) NULL,
name VARCHAR(255) NULL,
parent_source_id VARCHAR(128) NULL,
locked TINYINT(1) NOT NULL DEFAULT 0,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_source_cat (tenant_id, source_id, source_cat_id),
KEY idx_locked (tenant_id, locked),
CONSTRAINT fk_cm_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 8. ู
ูุงู
ุงูุงุณุชูุฑุงุฏ
CREATE TABLE {$prefix}sb_import_jobs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
source_id BIGINT UNSIGNED NOT NULL,
job_type ENUM('search','category','manual','bulk') NOT NULL DEFAULT 'manual',
status ENUM('queued','running','paused','completed','failed','cancelled') NOT NULL DEFAULT 'queued',
total_items INT UNSIGNED NOT NULL DEFAULT 0,
processed_items INT UNSIGNED NOT NULL DEFAULT 0,
failed_items INT UNSIGNED NOT NULL DEFAULT 0,
payload_json JSON NULL,
started_at DATETIME NULL,
finished_at DATETIME NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_tenant_status (tenant_id, status),
CONSTRAINT fk_ij_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 9. ู
ูุงู
AI
CREATE TABLE {$prefix}sb_ai_jobs (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
job_type ENUM('translate','rewrite','seo','categorize') NOT NULL,
provider VARCHAR(32) NOT NULL,
input_hash CHAR(64) NOT NULL,
output_json JSON NULL,
tokens_used INT UNSIGNED NOT NULL DEFAULT 0,
cost_usd DECIMAL(10,6) NOT NULL DEFAULT 0,
status ENUM('queued','running','completed','failed') NOT NULL DEFAULT 'queued',
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_hash (input_hash),
KEY idx_tenant (tenant_id),
CONSTRAINT fk_ai_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 10. ุงุณุชุฎุฏุงู
ุงูู
ูุฒุงุช
CREATE TABLE {$prefix}sb_usage (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
feature_key VARCHAR(64) NOT NULL,
period_start DATE NOT NULL,
used_value BIGINT UNSIGNED NOT NULL DEFAULT 0,
limit_value BIGINT UNSIGNED NOT NULL DEFAULT 0,
updated_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (id),
UNIQUE KEY uk_tenant_feature_period (tenant_id, feature_key, period_start)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 11. ุณุฌู ุงูููู
CREATE TABLE {$prefix}sb_lock_events (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NOT NULL,
action ENUM('lock','release') NOT NULL,
reason VARCHAR(64) NOT NULL,
products_affected INT UNSIGNED NOT NULL DEFAULT 0,
categories_affected INT UNSIGNED NOT NULL DEFAULT 0,
actor_id BIGINT UNSIGNED NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_tenant (tenant_id, created_at),
CONSTRAINT fk_le_tenant FOREIGN KEY (tenant_id) REFERENCES {$prefix}sb_tenants(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 12. ุณุฌู ุงูุชุฏููู
CREATE TABLE {$prefix}sb_audit_log (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
tenant_id BIGINT UNSIGNED NULL,
actor_id BIGINT UNSIGNED NULL,
action VARCHAR(64) NOT NULL,
object_type VARCHAR(64) NULL,
object_id BIGINT UNSIGNED NULL,
meta_json JSON NULL,
ip VARCHAR(45) NULL,
user_agent VARCHAR(255) NULL,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
KEY idx_tenant_time (tenant_id, created_at),
KEY idx_action (action)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- 13. DLQ
CREATE TABLE {$prefix}sb_dlq (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
job_type VARCHAR(64) NOT NULL,
payload LONGTEXT NOT NULL,
error TEXT NULL,
attempts TINYINT UNSIGNED NOT NULL DEFAULT 0,
failed_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
resolved_at DATETIME NULL,
PRIMARY KEY (id),
KEY idx_type_time (job_type, failed_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
๐งช ุงููุณู 23: ุงูุงุฎุชุจุงุฑุงุช (Testing Strategy)
23.1 ุงููุฑู
text
โโโโโโโโโโโโโโโ
โ E2E (5%) โ Playwright + WP test env
โโโโโโโโโโโโโโโค
โIntegration โ PHPUnit + wp-phpunit
โ (25%) โ
โโโโโโโโโโโโโโโค
โ Unit (70%) โ PHPUnit + Brain Monkey
โโโโโโโโโโโโโโโ
23.2 ุฃู ุซูุฉ ุงุฎุชุจุงุฑุงุช ุญุฑุฌุฉ
php
// tests/Unit/EntitlementCheckerTest.php
public function test_blocks_when_subscription_expired(): void {
$tenant = $this->factory->tenant(['status' => 'locked']);
$checker = new EntitlementChecker();
$this->assertFalse($checker->can($tenant->id, 'import_products', 1));
}
public function test_allows_when_within_quota(): void {
$tenant = $this->factory->tenant(['plan' => 'growth']);
$this->assertTrue((new EntitlementChecker())->can($tenant->id, 'import_products', 100));
}
// tests/Integration/LockManagerTest.php
public function test_lock_hides_all_products_on_salla(): void {
$mock = $this->mock_salla_client();
$mock->expects($this->exactly(50))->method('put')
->with($this->stringContains('/products/'), ['status' => 'hidden']);
$lock = Container::get(LockManager::class);
$count = $lock->lock($this->tenant->id, 'test');
$this->assertEquals(50, $count);
}
23.3 E2E Scenarios
- ู ุณุชุฃุฌุฑ ุฌุฏูุฏ โ ุชุณุฌูู โ ุฑุจุท ุณูุฉ โ ุงุณุชูุฑุงุฏ 10 ู ูุชุฌุงุช โ ุงูุชุญูู ู ู ุงูุธููุฑ.
- ุงุดุชุฑุงู โ ุงูุชูุงุก โ ููู ุชููุงุฆู โ ุชุฌุฏูุฏ โ ุฑูุน ุงูููู.
- AI translate 100 ูุต โ ุงูุชุญูู ู ู ุงูุชุฎุฒูู ูุงููุงุชูุฑุฉ.
๐ ุงููุณู 24: DevOps ูุงููุดุฑ
24.1 CI/CD Pipeline
yaml
# .github/workflows/ci.yml
name: CI
on: [push, pull_request]
jobs:
test:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: shivammathur/setup-php@v2
with: { php-version: '8.2' }
- run: composer install
- run: composer lint # PHPCS + PSR-12
- run: composer test # PHPUnit
- run: composer phpstan # Level 6
- run: npm ci && npm run build
24.2 Release Process
npm run buildย โยassets/dist/composer install --no-dev --optimize-autoloaderwp i18n make-pot . languages/salla-bridge.potwp dist-archive .ย โยsalla-bridge-x.y.z.zip- GitHub Release + ุชุญุฏูุซย
readme.txt.
24.3 Monitoring
| ุงูู ููุงุณ | ุงูุฃุฏุงุฉ |
|---|---|
| Uptime | UptimeRobot |
| Errors | Sentry (PHP SDK) |
| Performance | New Relic / Query Monitor |
| Business | Metabase ุนูู replica DB |
| Logs | ู
ููุงุช ููู
ูุฉ ูู wp-content/uploads/sb-logs/ |
24.4 Backup Strategy
- DB: ูุณุฎุฉ ูู 6 ุณุงุนุงุช โ S3.
wp-content/uploads/sb-*: ูุณุฎุฉ ููู ูุฉ.- ุงูุงุญุชูุงุธ: 30 ููู ูุง.
๐ ุงููุณู 25: ุงูุฃุฏุงุก ูุงูุชูุณุน (Performance & Scale)
25.1 ุงูููุงุณุงุช ุงูู ุณุชูุฏูุฉ
| ุงูุนู ููุฉ | ุงููุฏู |
|---|---|
| ุงุณุชูุฑุงุฏ ู ูุชุฌ ูุงุญุฏ | < 2s |
| ุงุณุชูุฑุงุฏ 1000 ู ูุชุฌ | < 30 ุฏูููุฉ |
| Lock tenant (10K ู ูุชุฌ) | < 5 ุฏูุงุฆู |
| REST response p95 | < 300ms |
| Dashboard load | < 1.5s |
25.2 ุงุณุชุฑุงุชูุฌูุงุช ุงูุชุญุณูู
- Indexes:ย ูู ุง ูู SQL ุฃุนูุงู.
- Chunking:ย ุฏุงุฆู
ูุงย
array_chunk(..., 50). - Object Cache:ย Redis/Memcached ููู
Tenant,ยPlan,ยEntitlements. - DB Read Replica:ย ูููDashboard queries.
- CDN:ย ููุตูุฑ ูุจู ุฑูุนูุง ูุณูุฉ.
- HTTP/2 + Connection Poolย ูู ุตุงุฏุฑ ุฎุงุฑุฌูุฉ.
25.3 Circuit Breaker ููู ุตุงุฏุฑ
php
final class CircuitBreaker {
public static function call(string $service, callable $fn): mixed {
if (get_transient("sb_cb_open_{$service}")) {
throw new \RuntimeException("Circuit open for {$service}");
}
try {
$res = $fn();
delete_transient("sb_cb_fail_{$service}");
return $res;
} catch (\Throwable $e) {
$fails = (int) get_transient("sb_cb_fail_{$service}") + 1;
set_transient("sb_cb_fail_{$service}", $fails, 300);
if ($fails >= 5) {
set_transient("sb_cb_open_{$service}", 1, 60);
}
throw $e;
}
}
}
๐ก๏ธ ุงููุณู 26: ู ุนุงูุฌุฉ ุงูุฃุฎุทุงุก ูุงูู ุฑุงูุจุฉ
26.1 Error Codes ุงูู ูุญูุฏุฉ
| Code | ุงูู ุนูู | HTTP |
|---|---|---|
SB_1001 | Entitlement exceeded | 402 |
SB_1002 | Subscription locked | 403 |
SB_1003 | Invalid source credentials | 401 |
SB_1004 | Salla rate limited | 429 |
SB_1005 | AI provider failed | 502 |
SB_1006 | Product not found | 404 |
SB_1007 | Duplicate import | 409 |
SB_2001 | Internal error | 500 |
26.2 Logger
php
final class Logger {
public function info(string $msg, array $ctx = []): void { $this->write('info', $msg, $ctx); }
public function error(string $msg, array $ctx = []): void {
$this->write('error', $msg, $ctx);
if (function_exists('sentry_capture_exception')) {
\Sentry\captureMessage($msg, \Sentry\Severity::error());
}
}
private function write(string $level, string $msg, array $ctx): void {
$file = WP_CONTENT_DIR . '/uploads/sb-logs/' . date('Y-m-d') . ".log";
wp_mkdir_p(dirname($file));
@file_put_contents($file, sprintf(
"[%s] %s: %s %s\n",
date('c'), strtoupper($level), $msg, wp_json_encode($ctx)
), FILE_APPEND | LOCK_EX);
}
}
๐จ ุงููุณู 27: ูุงุฌูุฉ ุงูู ุณุชุฎุฏู (UI/UX)
27.1 ุงูุตูุญุงุช ุงูุฑุฆูุณูุฉ
| ุงูุตูุญุฉ | ุงูุนูุงุตุฑ |
|---|---|
| Onboarding | Wizard 4 ุฎุทูุงุช: ุญุณุงุจ โ ุจุงูุฉ โ ุฑุจุท ุณูุฉ โ ุฃูู ุงุณุชูุฑุงุฏ |
| Dashboard | 4 ุจุทุงูุงุช KPI + ุฑุณู + ุฌุฏูู ุขุฎุฑ ุงูู ูุงู + ุชูุจูู ุงูุงุดุชุฑุงู |
| Import | ุงุฎุชูุงุฑ ู ุตุฏุฑุ ุจุญุซุ ููุชุฑุฉุ ุฅุถุงูุฉ ููุณูุฉุ ุงุณุชูุฑุงุฏ ุจุงูุฌู ูุฉ |
| Products | ุฌุฏูู 10K ุตู ุงูุชุฑุงุถู (Virtual Scroll)ุ ููุชุฑุฉุ bulk actions |
| Categories | ุดุฌุฑุฉ ูุงุจูุฉ ููุณุญุจุ mapping |
| AI Tools | ุชุฑุฌู ุฉุ SEOุ ุฅุนุงุฏุฉ ูุชุงุจุฉ |
| Billing | ุฎุทุทุ ููุงุชูุฑุ ุทุฑููุฉ ุฏูุนุ ุฅูุบุงุก |
| Settings | API keysุ webhooksุ timezoneุ ุชูุถููุงุช |
| Logs | audit + lock events + DLQ |
27.2 Stack ุงููุงุฌูุฉ
- React 18 + TypeScript
- Vite ููุจูุงุก
- Tailwind CSS
- TanStack Query ูููstate
@wordpress/dataย ููุชูุงู ู ู ุน WP admin- RTL native (ุฏุนู ุนุฑุจู ูุงู ู)
27.3 ุงูู ูููุงุช ุงูู ุดุชุฑูุฉ
<PlanCard>, <QuotaMeter>, <LockBanner>, <SourceSelector>, <ImportWizard>, <ProductTable>, <AiPanel>.
๐ ุงููุณู 28: ุฎุงุฑุทุฉ ุงูุทุฑูู (Roadmap)
| ุงูู ุฑุญูุฉ | ุงูู ุฏุฉ | ุงูู ุฎุฑุฌุงุช |
|---|---|---|
| v1.0 MVP | 8 ุฃุณุงุจูุน | Core + Salla + 1 source (AliExpress) + Billing + Lock |
| v1.1 | 4 ุฃุณุงุจูุน | 3 ู ุตุงุฏุฑ ุฅุถุงููุฉ + AI ู ุชูุงู ู + Dashboards ูุงู ูุฉ |
| v1.2 | 4 ุฃุณุงุจูุน | Multi-store + Mobile API + Webhooks ูุงู ูุฉ |
| v2.0 | 8 ุฃุณุงุจูุน | Multi-currency + Marketplace ููู ูุงุตูุงุช + Plugin SDK |
| v2.1 | 4 ุฃุณุงุจูุน | WhatsApp/Twitter notifications + Auto-scaling on K8s |
| v3.0 | โ | Mobile App (Flutter) + Public API ููู ุทูุฑูู |
โ ุงููุณู 29: Definition of Done (DoD)
ูุฃู ู ูู ุฉ:
- โกย Unit tests โฅ 80% coverage
- โกย Integration test ููู ุณุงุฑ ุงููุงู ู
- โกย PHPCS + PHPStan level 6 ูู ุฑูุงู
- โกย ุงูุชูุซูู ู
ุญุฏูุซ ููย
/docs - โกย
CHANGELOG.mdย ูุฐูุฑ ุงูุชุบููุฑ - โกย Migration ุขู ูุฉ (rollback ู ุชุงุญ)
- โกย Audit log ูุณุฌูู ุงูุนู ููุฉ
- โกย ู ุฑุงุฌุนุฉ ุฃู ุงู (checklist)
- โกย i18n strings ู ุญุถูุฑุฉ
- โกย Performance ู ูุงุณ
๐งญ ุงููุณู 30: ุงูุฎุทูุฉ ุงูุชุงููุฉ (Next Action)
ููุจุฏุก ููุฑูุง:
composer create-projectย ุฃู ุฃูุดุฆ ูููู ุงูู ุฌูุฏุงุช.- ุงูุณุฎ SQL ู
ู ุงููุณู
22 ุฅููย
Installer.php. - ูููุฐ ุฎุทุฉ ุงูู60 ุฏูููุฉ (ุงููุณู 11).
- ุดุบููย
CheckSubscriptionsย ูุฏูููุง ูุงุฎุชุจุงุฑ ุงูููู. - ุงุฑุจุท ุฃูู ู ุตุฏุฑ (AliExpress) ุนุจุฑ Sandbox.
- ุงุฎุชุจุฑ ุฏูุฑุฉ: ุชุณุฌูู โ ุงุดุชุฑุงู โ ุงูุชูุงุก โ ููู โ ุชุฌุฏูุฏ โ ุฑูุน.
ูู ุชุฑูุฏ ุฃู ุฃุจุฏุฃ ุงูุขู ุจูุชุงุจุฉ ุงูููุฏ ุงููุนูู ูุฃู ู
ูู ุจุนูููุ (ู
ุซููุง: Installer.php ูุงู
ููุงุ LockManager.phpุ Client.phpุ ุฃู React Dashboard). ุงุฎุชุฑ ูุงุจุฏุฃ.
๐๏ธ ุงูุฌุฒุก 2: ูุงุนุฏุฉ ุงูุจูุงูุงุช
โ ุงูู ูุงุตูุฉ ุงููุงู ูุฉ
Convention:
- Prefix:ย
kosmos_ - Engine: InnoDB
- Charset:ย
utf8mb4_unicode_ci - ูู ุงูุฌุฏุงูู ููุง:ย
id,ยcreated_at,ยupdated_at,ยdeleted_atย (soft delete)
๐ ุฌุฏูู 1: kosmos_tenants โ ุงูุญุณุงุจุงุช ุงูุฑุฆูุณูุฉ
sql
CREATE TABLE kosmos_tenants (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE,
slug VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(191) NOT NULL,
owner_user_id BIGINT UNSIGNED NOT NULL,
plan_slug VARCHAR(32) NOT NULL DEFAULT 'trial',
status ENUM('active','suspended','cancelled','trial') DEFAULT 'trial',
-- Domain & branding
subdomain VARCHAR(64) NOT NULL UNIQUE,
custom_domain VARCHAR(191) NULL UNIQUE,
logo_url VARCHAR(500) NULL,
primary_color VARCHAR(7) DEFAULT '#7c3aed',
-- Salla integration
salla_store_id VARCHAR(64) NULL,
salla_access_token TEXT NULL,
salla_refresh_token TEXT NULL,
salla_token_expires DATETIME NULL,
salla_webhook_secret VARCHAR(64) NULL,
-- Limits (denormalized for speed)
max_products INT DEFAULT 100,
max_users INT DEFAULT 3,
max_sources INT DEFAULT 2,
ai_credits INT DEFAULT 500,
-- Trial & billing
trial_ends_at DATETIME NULL,
current_period_start DATETIME NULL,
current_period_end DATETIME NULL,
metadata JSON NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_status (status),
INDEX idx_plan (plan_slug),
INDEX idx_owner (owner_user_id),
INDEX idx_trial_end (trial_ends_at),
INDEX idx_period_end (current_period_end)
) ENGINE=InnoDB;
๐ ุฌุฏูู 2: kosmos_subscriptions โ ุงูุงุดุชุฑุงูุงุช
sql
CREATE TABLE kosmos_subscriptions (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
plan_slug VARCHAR(32) NOT NULL,
-- Billing cycle
billing_cycle ENUM('monthly','yearly','lifetime') NOT NULL,
amount DECIMAL(10,2) NOT NULL,
currency CHAR(3) DEFAULT 'SAR',
-- Status
status ENUM('pending','active','past_due','suspended',
'cancelled','expired','paused') DEFAULT 'pending',
-- Periods
started_at DATETIME NULL,
current_period_start DATETIME NOT NULL,
current_period_end DATETIME NOT NULL,
cancelled_at DATETIME NULL,
paused_at DATETIME NULL,
grace_period_ends_at DATETIME NULL, -- 3 ุฃูุงู
ุจุนุฏ ุงููุดู
-- Payment gateway
gateway ENUM('stripe','paypal','moyasar','tap','manual') NOT NULL,
gateway_subscription_id VARCHAR(191) NULL,
gateway_customer_id VARCHAR(191) NULL,
-- Dunning
failed_payment_count INT DEFAULT 0,
last_payment_attempt DATETIME NULL,
next_retry_at DATETIME NULL,
metadata JSON NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
INDEX idx_tenant_status (tenant_id, status),
INDEX idx_period_end (current_period_end),
INDEX idx_grace (grace_period_ends_at),
INDEX idx_retry (next_retry_at)
) ENGINE=InnoDB;
๐ ุฌุฏูู 3: kosmos_entitlements โ ุตูุงุญูุงุช ุงููุตูู (ุงูููุจ)
sql
CREATE TABLE kosmos_entitlements (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
feature_key VARCHAR(64) NOT NULL, -- 'products.sync', 'ai.tutor', 'export.salla'
-- Scope
scope_type ENUM('global','source','target','product') DEFAULT 'global',
scope_id BIGINT UNSIGNED NULL,
-- Limits
limit_value INT NULL, -- NULL = unlimited
used_value INT DEFAULT 0,
reset_period ENUM('never','daily','monthly','yearly') DEFAULT 'monthly',
last_reset_at DATETIME NULL,
-- Flags
is_enabled TINYINT(1) DEFAULT 1,
expires_at DATETIME NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
UNIQUE KEY uk_tenant_feature_scope (tenant_id, feature_key, scope_type, scope_id),
INDEX idx_feature (feature_key),
INDEX idx_expires (expires_at)
) ENGINE=InnoDB;
๐ ุฌุฏูู 4: kosmos_plans โ ุชุนุฑูู ุงูุจุงูุงุช
sql
CREATE TABLE kosmos_plans (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
slug VARCHAR(32) NOT NULL UNIQUE,
name VARCHAR(191) NOT NULL,
description TEXT NULL,
-- Pricing
price_monthly DECIMAL(10,2) NOT NULL,
price_yearly DECIMAL(10,2) NOT NULL,
currency CHAR(3) DEFAULT 'SAR',
trial_days INT DEFAULT 14,
-- Limits (JSON ูููุฑุงุกุฉ ุงูุณุฑูุนุฉ)
limits JSON NOT NULL,
/* ู
ุซุงู:
{
"products": 500,
"sources": 5,
"targets": 3,
"users": 10,
"ai_credits": 5000,
"storage_mb": 2048
}
*/
features JSON NOT NULL,
/* ู
ุซุงู:
{
"salla_integration": true,
"shopify_integration": true,
"ai_tutor": true,
"knowledge_graph": true,
"white_label": false,
"api_access": false,
"priority_support": false
}
*/
is_active TINYINT(1) DEFAULT 1,
sort_order INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
) ENGINE=InnoDB;
๐ ุฌุฏูู 5: kosmos_sources โ ู
ุตุงุฏุฑ ุงูู
ุญุชูู
sql
CREATE TABLE kosmos_sources (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
source_type ENUM('shopify','woocommerce','salla','aliexpress',
'amazon','csv','json','api_custom','rss') NOT NULL,
name VARCHAR(191) NOT NULL,
-- Credentials (encrypted)
credentials TEXT NULL, -- AES-256 encrypted JSON
config JSON NULL, -- filters, categories, etc.
-- Sync state
status ENUM('idle','syncing','error','disabled') DEFAULT 'idle',
last_sync_at DATETIME NULL,
last_error TEXT NULL,
sync_frequency ENUM('manual','hourly','daily','weekly') DEFAULT 'daily',
next_sync_at DATETIME NULL,
-- Stats
total_products INT DEFAULT 0,
synced_products INT DEFAULT 0,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
INDEX idx_tenant_type (tenant_id, source_type),
INDEX idx_next_sync (next_sync_at)
) ENGINE=InnoDB;
๐ ุฌุฏูู 6: kosmos_targets โ ูููุงุช ุงูุฅุฎุฑุงุฌ (ุณูุฉุ ุดูุจููุงู…)
sql
CREATE TABLE kosmos_targets (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
target_type ENUM('salla','shopify','woocommerce','custom') NOT NULL,
name VARCHAR(191) NOT NULL,
-- Credentials
credentials TEXT NOT NULL, -- encrypted
store_url VARCHAR(500) NULL,
store_id VARCHAR(191) NULL,
-- Sync policy
auto_publish TINYINT(1) DEFAULT 0,
auto_update TINYINT(1) DEFAULT 1,
auto_delete TINYINT(1) DEFAULT 0, -- ุนูุฏ ูุดู ุงูุงุดุชุฑุงู
-- Lock state
is_locked TINYINT(1) DEFAULT 0,
locked_at DATETIME NULL,
lock_reason VARCHAR(64) NULL, -- 'subscription_expired', 'manual'
-- Stats
published_count INT DEFAULT 0,
last_publish_at DATETIME NULL,
last_error TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
INDEX idx_tenant_type (tenant_id, target_type),
INDEX idx_locked (is_locked)
) ENGINE=InnoDB;
๐ ุฌุฏูู 7: kosmos_products โ ูุชุงููุฌ ู
ูุญูุฏ
sql
CREATE TABLE kosmos_products (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
source_id BIGINT UNSIGNED NULL,
-- Identity
external_id VARCHAR(191) NULL, -- ID ูู ุงูู
ุตุฏุฑ
sku VARCHAR(100) NULL,
hash CHAR(64) NOT NULL, -- SHA256 ููุชุบููุฑุงุช
-- Content
title VARCHAR(500) NOT NULL,
description LONGTEXT NULL,
short_desc TEXT NULL,
-- Pricing
price DECIMAL(10,2) NULL,
compare_price DECIMAL(10,2) NULL,
cost_price DECIMAL(10,2) NULL,
currency CHAR(3) DEFAULT 'SAR',
-- Inventory
stock INT NULL,
stock_status ENUM('in_stock','out_of_stock','backorder') DEFAULT 'in_stock',
-- Media
images JSON NULL, -- [{url, alt, order}]
videos JSON NULL,
primary_image VARCHAR(500) NULL,
-- Categorization
categories JSON NULL, -- [{id, name, path}]
tags JSON NULL,
brand VARCHAR(191) NULL,
-- Type
product_type ENUM('simple','variable','digital','service',
'subscription','bundle') DEFAULT 'simple',
variants JSON NULL,
-- AI enrichment
ai_summary TEXT NULL,
ai_tags JSON NULL,
embedding_id VARCHAR(64) NULL, -- Qdrant point ID
-- Sync status
sync_status ENUM('pending','synced','outdated','error','locked') DEFAULT 'pending',
locked_by_subscription TINYINT(1) DEFAULT 0,
metadata JSON NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
FOREIGN KEY (source_id) REFERENCES kosmos_sources(id) ON DELETE SET NULL,
UNIQUE KEY uk_tenant_source_external (tenant_id, source_id, external_id),
INDEX idx_tenant_status (tenant_id, sync_status),
INDEX idx_hash (hash),
INDEX idx_locked (locked_by_subscription)
) ENGINE=InnoDB;
๐ ุฌุฏูู 8: kosmos_publish_log โ ุณุฌู ุงููุดุฑ
sql
CREATE TABLE kosmos_publish_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
product_id BIGINT UNSIGNED NOT NULL,
target_id BIGINT UNSIGNED NOT NULL,
action ENUM('create','update','delete','lock','unlock','skip') NOT NULL,
status ENUM('pending','success','failed','retrying') DEFAULT 'pending',
external_id VARCHAR(191) NULL, -- ID ูู ุงููุฏู
request_payload JSON NULL,
response_body TEXT NULL,
error_message TEXT NULL,
attempts INT DEFAULT 1,
duration_ms INT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
completed_at DATETIME NULL,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
FOREIGN KEY (product_id) REFERENCES kosmos_products(id) ON DELETE CASCADE,
FOREIGN KEY (target_id) REFERENCES kosmos_targets(id) ON DELETE CASCADE,
INDEX idx_tenant_status (tenant_id, status),
INDEX idx_target (target_id, created_at),
INDEX idx_product (product_id, created_at)
) ENGINE=InnoDB;
๐ ุฌุฏูู 9: kosmos_ai_usage โ ุงุณุชููุงู AI
sql
CREATE TABLE kosmos_ai_usage (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
user_id BIGINT UNSIGNED NULL,
feature VARCHAR(64) NOT NULL, -- 'tutor', 'enrich', 'translate', 'generate'
model VARCHAR(64) NOT NULL,
provider ENUM('openai','anthropic','google','local') NOT NULL,
tokens_in INT UNSIGNED DEFAULT 0,
tokens_out INT UNSIGNED DEFAULT 0,
credits_used DECIMAL(10,4) DEFAULT 0,
cost_usd DECIMAL(10,6) DEFAULT 0,
duration_ms INT NULL,
success TINYINT(1) DEFAULT 1,
error_message TEXT NULL,
context_id VARCHAR(191) NULL, -- ูุฑุจุทูุง ุจู
ูุชุฌ ุฃู ู
ุญุงุฏุซุฉ
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
INDEX idx_tenant_date (tenant_id, created_at),
INDEX idx_feature (feature),
INDEX idx_user (user_id)
) ENGINE=InnoDB;
๐ ุฌุฏูู 10: kosmos_audit_log โ ุงูุฃู
ุงู ูุงูุงู
ุชุซุงู
sql
CREATE TABLE kosmos_audit_log (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NULL,
user_id BIGINT UNSIGNED NULL,
action VARCHAR(64) NOT NULL,
entity_type VARCHAR(64) NULL,
entity_id BIGINT UNSIGNED NULL,
old_value JSON NULL,
new_value JSON NULL,
ip_address VARCHAR(45) NULL,
user_agent VARCHAR(500) NULL,
severity ENUM('info','warning','error','critical') DEFAULT 'info',
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_tenant_date (tenant_id, created_at),
INDEX idx_entity (entity_type, entity_id),
INDEX idx_severity (severity, created_at)
) ENGINE=InnoDB;
๐ ุฌุฏูู 11: kosmos_jobs โ ุทุงุจูุฑ ุงูู
ูุงู
sql
CREATE TABLE kosmos_jobs (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NOT NULL,
job_type VARCHAR(64) NOT NULL, -- 'sync_source', 'publish_product', 'bulk_lock'
payload JSON NOT NULL,
status ENUM('queued','running','completed','failed','cancelled') DEFAULT 'queued',
priority TINYINT DEFAULT 5, -- 1 = highest
attempts INT DEFAULT 0,
max_attempts INT DEFAULT 3,
scheduled_at DATETIME DEFAULT CURRENT_TIMESTAMP,
started_at DATETIME NULL,
completed_at DATETIME NULL,
result JSON NULL,
error_message TEXT NULL,
worker_id VARCHAR(64) NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (tenant_id) REFERENCES kosmos_tenants(id) ON DELETE CASCADE,
INDEX idx_status_priority (status, priority, scheduled_at),
INDEX idx_tenant (tenant_id, created_at)
) ENGINE=InnoDB;
๐ ุฌุฏูู 12: kosmos_webhook_events โ ุงูุฃุญุฏุงุซ ุงููุงุฑุฏุฉ
sql
CREATE TABLE kosmos_webhook_events (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
tenant_id BIGINT UNSIGNED NULL,
source ENUM('salla','shopify','stripe','paypal','moyasar','internal') NOT NULL,
event_type VARCHAR(64) NOT NULL,
event_id VARCHAR(191) NULL UNIQUE,
payload JSON NOT NULL,
signature VARCHAR(500) NULL,
signature_valid TINYINT(1) DEFAULT 0,
processed TINYINT(1) DEFAULT 0,
processed_at DATETIME NULL,
error_message TEXT NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
INDEX idx_source_event (source, event_type),
INDEX idx_processed (processed, created_at)
) ENGINE=InnoDB;
๐ ู ุฎุทุท ุงูุนูุงูุงุช (ERD)
๐งฉ ุงูุฌุฒุก 3: ูููู Plugin ุงููุงู ู
text
kosmos-studio/
โโโ kosmos-studio.php # Main plugin file
โโโ composer.json
โโโ composer.lock
โโโ readme.txt
โโโ uninstall.php
โโโ .env.example
โ
โโโ src/
โ โโโ Core/
โ โ โโโ Plugin.php # Bootstrap
โ โ โโโ Container.php # Simple DI container
โ โ โโโ Config.php
โ โ โโโ Hooks.php # Hook loader
โ โ โโโ Constants.php
โ โ
โ โโโ Database/
โ โ โโโ Migrator.php
โ โ โโโ Schema.php
โ โ โโโ QueryBuilder.php
โ โ โโโ Migrations/
โ โ โโโ Migration_001_CreateTenants.php
โ โ โโโ Migration_002_CreateSubscriptions.php
โ โ โโโ Migration_003_CreateEntitlements.php
โ โ โโโ Migration_004_CreatePlans.php
โ โ โโโ Migration_005_CreateSources.php
โ โ โโโ Migration_006_CreateTargets.php
โ โ โโโ Migration_007_CreateProducts.php
โ โ โโโ Migration_008_CreatePublishLog.php
โ โ โโโ Migration_009_CreateAiUsage.php
โ โ โโโ Migration_010_CreateAuditLog.php
โ โ โโโ Migration_011_CreateJobs.php
โ โ โโโ Migration_012_CreateWebhookEvents.php
โ โ
โ โโโ Domain/
โ โ โโโ Tenant/
โ โ โ โโโ Tenant.php # Entity
โ โ โ โโโ TenantRepository.php
โ โ โ โโโ TenantService.php
โ โ โ โโโ TenantProvisioner.php # ุฅูุดุงุก tenant ุฌุฏูุฏ
โ โ โ
โ โ โโโ Subscription/
โ โ โ โโโ Subscription.php
โ โ โ โโโ SubscriptionRepository.php
โ โ โ โโโ SubscriptionService.php
โ โ โ โโโ Plan.php
โ โ โ โโโ PlanRepository.php
โ โ โ โโโ DunningManager.php # ุฅุฏุงุฑุฉ ูุดู ุงูุฏูุน
โ โ โ
โ โ โโโ Entitlement/
โ โ โ โโโ Entitlement.php
โ โ โ โโโ EntitlementRepository.php
โ โ โ โโโ EntitlementService.php # can(), consume(), grant()
โ โ โ โโโ Feature.php # Enum-like
โ โ โ
โ โ โโโ Product/
โ โ โ โโโ Product.php
โ โ โ โโโ ProductRepository.php
โ โ โ โโโ ProductService.php
โ โ โ โโโ ProductLockManager.php # ๐ ููู ุงูู
ูุชุฌุงุช
โ โ โ
โ โ โโโ Source/
โ โ โ โโโ Source.php
โ โ โ โโโ SourceRepository.php
โ โ โ โโโ Adapters/
โ โ โ โโโ SourceAdapterInterface.php
โ โ โ โโโ ShopifyAdapter.php
โ โ โ โโโ WooCommerceAdapter.php
โ โ โ โโโ SallaAdapter.php
โ โ โ โโโ AliExpressAdapter.php
โ โ โ โโโ CsvAdapter.php
โ โ โ โโโ CustomApiAdapter.php
โ โ โ
โ โ โโโ Target/
โ โ โโโ Target.php
โ โ โโโ TargetRepository.php
โ โ โโโ Publishers/
โ โ โโโ TargetPublisherInterface.php
โ โ โโโ SallaPublisher.php # โ ุงูุฃูู
โ โ โโโ ShopifyPublisher.php
โ โ โโโ WooCommercePublisher.php
โ โ
โ โโโ Application/
โ โ โโโ Bridge/
โ โ โ โโโ SyncOrchestrator.php # ูุฏูุฑ ุนู
ููุฉ ุงูุณุญุจ
โ โ โ โโโ PublishOrchestrator.php # ูุฏูุฑ ุนู
ููุฉ ุงูุฏูุน
โ โ โ โโโ LockOrchestrator.php # ๐ ูุฏูุฑ ุงูููู
โ โ โ
โ โ โโโ AI/
โ โ โ โโโ AIGateway.php
โ โ โ โโโ Providers/
โ โ โ โ โโโ OpenAIProvider.php
โ โ โ โ โโโ AnthropicProvider.php
โ โ โ โ โโโ LocalProvider.php
โ โ โ โโโ Features/
โ โ โ โ โโโ ProductEnricher.php
โ โ โ โ โโโ TutorAgent.php
โ โ โ โ โโโ Translator.php
โ โ โ โ โโโ DescriptionGenerator.php
โ โ โ โโโ MeteringService.php
โ โ โ
โ โ โโโ Jobs/
โ โ โโโ JobInterface.php
โ โ โโโ JobQueue.php
โ โ โโโ Worker.php
โ โ โโโ Handlers/
โ โ โโโ SyncSourceJob.php
โ โ โโโ PublishProductJob.php
โ โ โโโ BulkLockJob.php
โ โ โโโ RetryPaymentJob.php
โ โ
โ โโโ Integrations/
โ โ โโโ Salla/
โ โ โ โโโ SallaClient.php # HTTP Client
โ โ โ โโโ SallaOAuth.php
โ โ โ โโโ SallaWebhooks.php
โ โ โ โโโ SallaMapper.php # Product โ Salla format
โ โ โ
โ โ โโโ Shopify/
โ โ โ โโโ ShopifyClient.php
โ โ โ โโโ ShopifyMapper.php
โ โ โ
โ โ โโโ Payments/
โ โ โโโ StripeGateway.php
โ โ โโโ MoyasarGateway.php
โ โ โโโ TapGateway.php
โ โ โโโ PayPalGateway.php
โ โ
โ โโโ Http/
โ โ โโโ Router.php
โ โ โโโ Middleware/
โ โ โ โโโ AuthMiddleware.php
โ โ โ โโโ TenantMiddleware.php # โ ูุญุฏุฏ ุงููtenant ุงูุญุงูู
โ โ โ โโโ EntitlementMiddleware.php# โ ูู
ูุน ุงููุตูู ุจุฏูู ุตูุงุญูุฉ
โ โ โ โโโ RateLimitMiddleware.php
โ โ โโโ Controllers/
โ โ โโโ AuthController.php
โ โ โโโ TenantController.php
โ โ โโโ SubscriptionController.php
โ โ โโโ SourceController.php
โ โ โโโ TargetController.php
โ โ โโโ ProductController.php
โ โ โโโ AiController.php
โ โ โโโ WebhookController.php
โ โ
โ โโโ Admin/
โ โ โโโ Menu.php
โ โ โโโ Dashboard.php
โ โ โโโ Pages/
โ โ โ โโโ TenantsPage.php
โ โ โ โโโ PlansPage.php
โ โ โ โโโ SourcesPage.php
โ โ โ โโโ TargetsPage.php
โ โ โ โโโ ProductsPage.php
โ โ โ โโโ SettingsPage.php
โ โ โโโ Views/
โ โ โโโ dashboard.php
โ โ โโโ sources-list.php
โ โ โโโ products-grid.php
โ โ โโโ lock-status.php
โ โ
โ โโโ Frontend/
โ โ โโโ Shortcodes/
โ โ โ โโโ PricingShortcode.php
โ โ โ โโโ DashboardShortcode.php
โ โ โ โโโ TutorShortcode.php
โ โ โโโ Blocks/
โ โ โ โโโ PricingBlock.php
โ โ โ โโโ ProductsBlock.php
โ โ โโโ Assets/
โ โ โโโ css/
โ โ โโโ js/
โ โ
โ โโโ Support/
โ โ โโโ Logger.php
โ โ โโโ Encrypter.php # AES-256 for credentials
โ โ โโโ Validator.php
โ โ โโโ Str.php
โ โ โโโ Arr.php
โ โ โโโ Http.php
โ โ
โ โโโ Exceptions/
โ โโโ EntitlementException.php
โ โโโ SubscriptionExpiredException.php
โ โโโ SourceException.php
โ โโโ TargetException.php
โ
โโโ config/
โ โโโ plans.php # ุชุนุฑูู ุงูุจุงูุงุช
โ โโโ features.php # ูุงุฆู
ุฉ ุงูู
ูุฒุงุช
โ โโโ ai-models.php
โ
โโโ database/
โ โโโ seeds/
โ โโโ PlansSeeder.php
โ โโโ FeaturesSeeder.php
โ
โโโ tests/
โ โโโ Unit/
โ โโโ Integration/
โ โโโ E2E/
โ
โโโ assets/
โ โโโ admin/
โ โโโ public/
โ
โโโ languages/
โโโ kosmos-ar.po
โโโ kosmos-en_US.po
๐ป ุงูุฌุฒุก 4: PHP Namespaces & Classes
Convention:
text
KOSMOS\Studio\<Layer>\<Domain>\<Class>
ุงูู ูู ุงูุฑุฆูุณู:
php
<?php
/**
* Plugin Name: KOSMOS Studio
* Description: Subscription-as-a-Service Knowledge & Commerce Bridge
* Version: 1.0.0
* Author: KOSMOS Team
* Text Domain: kosmos
* Requires PHP: 8.2
* Requires at least: 6.4
*/
declare(strict_types=1);
namespace KOSMOS\Studio;
if (!defined('ABSPATH')) exit;
// Autoload
require_once __DIR__ . '/vendor/autoload.php';
// Constants
define('KOSMOS_VERSION', '1.0.0');
define('KOSMOS_FILE', __FILE__);
define('KOSMOS_PATH', plugin_dir_path(__FILE__));
define('KOSMOS_URL', plugin_dir_url(__FILE__));
define('KOSMOS_DB_VERSION', '1.0.0');
// Boot
Core\Plugin::instance()->boot();
Core\Plugin โ ููุทุฉ ุงูุฏุฎูู:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Core;
use KOSMOS\Studio\Database\Migrator;
use KOSMOS\Studio\Admin\Menu;
use KOSMOS\Studio\Http\Router;
use KOSMOS\Studio\Application\Jobs\Worker;
final class Plugin {
private static ?self $instance = null;
private Container $container;
public static function instance(): self {
return self::$instance ??= new self();
}
private function __construct() {
$this->container = new Container();
}
public function boot(): void {
// 1. Container bindings
$this->registerBindings();
// 2. Activation/Deactivation
register_activation_hook(KOSMOS_FILE, [$this, 'onActivate']);
register_deactivation_hook(KOSMOS_FILE, [$this, 'onDeactivate']);
// 3. Hooks
add_action('plugins_loaded', [$this, 'onPluginsLoaded']);
add_action('init', [$this, 'onInit']);
add_action('rest_api_init', [$this, 'registerRestRoutes']);
// 4. Workers (background)
if (defined('KOSMOS_ENABLE_WORKER') && KOSMOS_ENABLE_WORKER) {
add_action('kosmos_worker_tick', [Worker::class, 'tick']);
}
}
private function registerBindings(): void {
// Core services
$this->container->singleton(Config::class);
$this->container->singleton(Hooks::class);
// Domain services
$this->container->singleton(\KOSMOS\Studio\Domain\Tenant\TenantService::class);
$this->container->singleton(\KOSMOS\Studio\Domain\Subscription\SubscriptionService::class);
$this->container->singleton(\KOSMOS\Studio\Domain\Entitlement\EntitlementService::class);
$this->container->singleton(\KOSMOS\Studio\Domain\Product\ProductLockManager::class);
// Application services
$this->container->singleton(\KOSMOS\Studio\Application\Bridge\SyncOrchestrator::class);
$this->container->singleton(\KOSMOS\Studio\Application\Bridge\PublishOrchestrator::class);
$this->container->singleton(\KOSMOS\Studio\Application\Bridge\LockOrchestrator::class);
$this->container->singleton(\KOSMOS\Studio\Application\AI\AIGateway::class);
}
public function onActivate(): void {
// Run migrations
(new Migrator())->run();
// Seed plans
(new \KOSMOS\Studio\Database\Seeds\PlansSeeder())->run();
// Schedule cron
if (!wp_next_scheduled('kosmos_worker_tick')) {
wp_schedule_event(time(), 'every_minute', 'kosmos_worker_tick');
}
// Flush rewrite rules
flush_rewrite_rules();
// Set DB version
update_option('kosmos_db_version', KOSMOS_DB_VERSION);
}
public function onDeactivate(): void {
wp_clear_scheduled_hook('kosmos_worker_tick');
flush_rewrite_rules();
}
public function onPluginsLoaded(): void {
// Check if WooCommerce is active
if (!class_exists('WooCommerce')) {
add_action('admin_notices', function () {
echo '<div class="error"><p>';
echo 'KOSMOS Studio requires WooCommerce to be installed.';
echo '</p></div>';
});
return;
}
// Load translations
load_plugin_textdomain('kosmos', false, dirname(plugin_basename(KOSMOS_FILE)) . '/languages');
}
public function onInit(): void {
// Register post types, taxonomies if needed
// Register shortcodes
add_shortcode('kosmos_pricing', [\KOSMOS\Studio\Frontend\Shortcodes\PricingShortcode::class, 'render']);
add_shortcode('kosmos_dashboard', [\KOSMOS\Studio\Frontend\Shortcodes\DashboardShortcode::class, 'render']);
add_shortcode('kosmos_tutor', [\KOSMOS\Studio\Frontend\Shortcodes\TutorShortcode::class, 'render']);
}
public function registerRestRoutes(): void {
(new Router())->register();
}
public function container(): Container {
return $this->container;
}
}
Domain\Entitlement\EntitlementService โ ุฃูู class:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Domain\Entitlement;
use KOSMOS\Studio\Domain\Tenant\Tenant;
use KOSMOS\Studio\Exceptions\EntitlementException;
/**
* ููุทุฉ ุงููุฑุงุฑ ุงูู
ุฑูุฒูุฉ โ ูู ุดูุก ูู
ุฑ ู
ู ููุง
*/
final class EntitlementService {
public function __construct(
private EntitlementRepository $repo
) {}
/**
* ูู ูู
ูู ูููtenant ุงุณุชุฎุฏุงู
ุงูู
ูุฒุฉุ
*/
public function can(int $tenantId, string $featureKey, ?int $scopeId = null): bool {
$entitlement = $this->repo->find($tenantId, $featureKey, $scopeId);
if (!$entitlement) return false;
if (!$entitlement->isEnabled()) return false;
if ($entitlement->isExpired()) return false;
if ($entitlement->isExhausted()) return false;
return true;
}
/**
* ุงุณุชููู ู
ู ุงูุญุตุฉ
*/
public function consume(int $tenantId, string $featureKey, int $amount = 1): bool {
if (!$this->can($tenantId, $featureKey)) {
throw new EntitlementException(
"Entitlement exhausted: {$featureKey}"
);
}
return $this->repo->increment($tenantId, $featureKey, $amount);
}
/**
* ุงู
ูุญ/ุฌุฏูุฏ
*/
public function grant(int $tenantId, string $featureKey, int $limit, ?int $scopeId = null): void {
$this->repo->upsert($tenantId, $featureKey, [
'limit_value' => $limit,
'used_value' => 0,
'is_enabled' => 1,
'last_reset_at' => current_time('mysql'),
], $scopeId);
}
/**
* ุนุฒูู ูู entitlements ุนูุฏ ุฅูุบุงุก ุงูุงุดุชุฑุงู
*/
public function revokeAll(int $tenantId): void {
$this->repo->disableAll($tenantId);
}
/**
* ุจุทูู ุงูู
ูุฒุงุช ุงูู
ุฑุชุจุทุฉ ุจู
ุตุฏุฑ ู
ุนูู
*/
public function revokeForSource(int $tenantId, int $sourceId): void {
$this->repo->disableByScope($tenantId, 'source', $sourceId);
}
}
Domain\Product\ProductLockManager โ ๐ ููู ุงูู ูุชุฌุงุช:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Domain\Product;
/**
* ุงูู
ุณุคูู ุนู ููู/ูุชุญ ุงูู
ูุชุฌุงุช ุนูุฏ ุงูุงุดุชุฑุงู/ุงูุฅูุบุงุก
*/
final class ProductLockManager {
public function __construct(
private ProductRepository $products,
private \KOSMOS\Studio\Integrations\Salla\SallaClient $salla,
private \KOSMOS\Studio\Support\Logger $logger
) {}
/**
* ุงููู ูู ู
ูุชุฌุงุช ุงููtenant
*/
public function lockAll(int $tenantId, string $reason = 'subscription_expired'): int {
$this->logger->warning("Locking all products for tenant {$tenantId}: {$reason}");
// 1. ุนููู
ูู DB
$locked = $this->products->lockAll($tenantId);
// 2. ุฃุถู job ูุฅุฎูุงุก ุงูู
ูุชุฌุงุช ู
ู ุณูุฉ
foreach ($this->products->getTargetsByTenant($tenantId) as $target) {
\KOSMOS\Studio\Application\Jobs\JobQueue::push(
$tenantId,
'bulk_lock',
[
'target_id' => $target['id'],
'action' => 'hide',
'reason' => $reason,
],
priority: 1
);
}
// 3. ุณุฌูู ุงูุญุฏุซ
do_action('kosmos/products_locked', $tenantId, $locked, $reason);
return $locked;
}
/**
* ุงูุชุญ ุงูู
ูุชุฌุงุช ุนูุฏ ุชุฌุฏูุฏ ุงูุงุดุชุฑุงู
*/
public function unlockAll(int $tenantId): int {
$this->logger->info("Unlocking all products for tenant {$tenantId}");
$unlocked = $this->products->unlockAll($tenantId);
foreach ($this->products->getTargetsByTenant($tenantId) as $target) {
\KOSMOS\Studio\Application\Jobs\JobQueue::push(
$tenantId,
'bulk_lock',
[
'target_id' => $target['id'],
'action' => 'show',
],
priority: 1
);
}
do_action('kosmos/products_unlocked', $tenantId, $unlocked);
return $unlocked;
}
/**
* ุงููู ู
ุตุฏุฑุงู ู
ุนููุงู (ุนูุฏ ุญุฐู/ุชุนุทูู ู
ุตุฏุฑ)
*/
public function lockSource(int $tenantId, int $sourceId): int {
return $this->products->lockBySource($tenantId, $sourceId);
}
}
Application\Bridge\LockOrchestrator โ ุงูู ูุณู:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Application\Bridge;
use KOSMOS\Studio\Domain\Product\ProductLockManager;
use KOSMOS\Studio\Domain\Entitlement\EntitlementService;
use KOSMOS\Studio\Domain\Target\TargetRepository;
/**
* ููุณูู ุนู
ููุฉ ุงูููู ุงููุงู
ูุฉ ุนูุฏ ุงูุชูุงุก ุงูุงุดุชุฑุงู
*/
final class LockOrchestrator {
public function __construct(
private ProductLockManager $lockManager,
private EntitlementService $entitlements,
private TargetRepository $targets
) {}
/**
* ููุณุชุฏุนู ู
ู cron ุฃู webhook ุนูุฏ ุงูุชูุงุก ุงูุงุดุชุฑุงู
*/
public function handleSubscriptionExpired(int $tenantId): void {
// 1. ุนุฒูู ูู ุงููentitlements
$this->entitlements->revokeAll($tenantId);
// 2. ุงููู ูู ุงูู
ูุชุฌุงุช
$count = $this->lockManager->lockAll($tenantId, 'subscription_expired');
// 3. ุงููู ูู ุงููtargets
$this->targets->lockAll($tenantId, 'subscription_expired');
// 4. ุณุฌูู
do_action('kosmos/subscription_expired_handled', $tenantId, $count);
}
/**
* ุนูุฏ ุชุฌุฏูุฏ ุงูุงุดุชุฑุงู โ ุงูุชุญ ูู ุดูุก
*/
public function handleSubscriptionRenewed(int $tenantId): void {
// 1. ุฌุฏูุฏ ุงููentitlements ุจูุงุกู ุนูู ุงูุจุงูุฉ
$this->entitlements->grantFromPlan($tenantId);
// 2. ุงูุชุญ ุงูู
ูุชุฌุงุช
$this->lockManager->unlockAll($tenantId);
// 3. ุงูุชุญ ุงููtargets
$this->targets->unlockAll($tenantId);
do_action('kosmos/subscription_renewed_handled', $tenantId);
}
}
Integrations\Salla\SallaPublisher โ ุงููุดุฑ ูู ุณูุฉ:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Integrations\Salla;
use KOSMOS\Studio\Domain\Product\Product;
use KOSMOS\Studio\Domain\Target\Target;
/**
* ู
ุณุคูู ุนู ูุดุฑ/ุฅุฎูุงุก/ุชุญุฏูุซ ุงูู
ูุชุฌุงุช ูู ู
ุชุฌุฑ ุณูุฉ
*/
final class SallaPublisher {
public function __construct(
private SallaClient $client,
private SallaMapper $mapper
) {}
/**
* ุงูุดุฑ ู
ูุชุฌุงู ุฌุฏูุฏุงู ูู ุณูุฉ
*/
public function publish(Target $target, Product $product): array {
$payload = $this->mapper->toSalla($product);
$response = $this->client
->forTarget($target)
->post('/products', $payload);
if ($response['success'] ?? false) {
return [
'success' => true,
'external_id' => $response['data']['id'] ?? null,
];
}
return ['success' => false, 'error' => $response['message'] ?? 'unknown'];
}
/**
* ุญุฏูุซ ู
ูุชุฌุงู ู
ูุฌูุฏุงู
*/
public function update(Target $target, Product $product, string $externalId): array {
$payload = $this->mapper->toSalla($product);
$response = $this->client
->forTarget($target)
->put("/products/{$externalId}", $payload);
return [
'success' => $response['success'] ?? false,
'error' => $response['message'] ?? null,
];
}
/**
* ๐ ุฃุฎูู ู
ูุชุฌุงู (ุนูุฏ ุงูุชูุงุก ุงูุงุดุชุฑุงู)
*/
public function hide(Target $target, string $externalId): bool {
$response = $this->client
->forTarget($target)
->put("/products/{$externalId}", [
'status' => 'hidden',
'visibility' => 'hidden',
]);
return $response['success'] ?? false;
}
/**
* ุฃุธูุฑ ู
ูุชุฌุงู (ุนูุฏ ุงูุชุฌุฏูุฏ)
*/
public function show(Target $target, string $externalId): bool {
$response = $this->client
->forTarget($target)
->put("/products/{$externalId}", [
'status' => 'sale',
'visibility' => 'visible',
]);
return $response['success'] ?? false;
}
/**
* ุงุญุฐู ู
ูุชุฌุงู
*/
public function delete(Target $target, string $externalId): bool {
$response = $this->client
->forTarget($target)
->delete("/products/{$externalId}");
return $response['success'] ?? false;
}
/**
* ๐ ุงููู ุชุตูููุงู ูุงู
ูุงู (categories hidden)
*/
public function hideCategory(Target $target, string $categoryId): bool {
$response = $this->client
->forTarget($target)
->put("/categories/{$categoryId}", [
'status' => 'hidden',
]);
return $response['success'] ?? false;
}
/**
* ุงุฌูุจ ุงูู
ูุชุฌุงุช ู
ู ุณูุฉ (ูููsync ุงูุนูุณู)
*/
public function listProducts(Target $target, int $page = 1): array {
return $this->client
->forTarget($target)
->get('/products', ['page' => $page, 'per_page' => 50]);
}
}
Integrations\Salla\SallaClient:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Integrations\Salla;
use KOSMOS\Studio\Domain\Target\Target;
use KOSMOS\Studio\Support\Http;
final class SallaClient {
private const BASE_URL = 'https://api.salla.dev/admin/v2';
private ?Target $target = null;
public function forTarget(Target $target): self {
$clone = clone $this;
$clone->target = $target;
return $clone;
}
public function get(string $path, array $query = []): array {
return $this->request('GET', $path, $query);
}
public function post(string $path, array $body = []): array {
return $this->request('POST', $path, [], $body);
}
public function put(string $path, array $body = []): array {
return $this->request('PUT', $path, [], $body);
}
public function delete(string $path): array {
return $this->request('DELETE', $path);
}
private function request(string $method, string $path, array $query = [], array $body = []): array {
if (!$this->target) {
throw new \RuntimeException('No target set. Use forTarget() first.');
}
$creds = json_decode(
\KOSMOS\Studio\Support\Encrypter::decrypt($this->target->credentials),
true
);
$token = $creds['access_token'] ?? '';
// Handle token refresh if needed
if ($this->isTokenExpired($creds)) {
$token = $this->refreshToken($this->target, $creds);
}
$url = self::BASE_URL . $path;
if ($query) $url .= '?' . http_build_query($query);
$response = Http::request($method, $url, [
'headers' => [
'Authorization' => "Bearer {$token}",
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'body' => $body ? wp_json_encode($body) : null,
'timeout' => 30,
]);
// Log for audit
\KOSMOS\Studio\Support\Logger::debug("Salla API {$method} {$path}", [
'status' => $response['status'] ?? null,
]);
return $response;
}
private function isTokenExpired(array $creds): bool {
return isset($creds['expires_at'])
&& strtotime($creds['expires_at']) < time() + 300;
}
private function refreshToken(Target $target, array $creds): string {
$response = Http::post('https://accounts.salla.sa/oauth2/token', [
'grant_type' => 'refresh_token',
'refresh_token' => $creds['refresh_token'],
'client_id' => get_option('kosmos_salla_client_id'),
'client_secret' => get_option('kosmos_salla_client_secret'),
]);
$newCreds = array_merge($creds, [
'access_token' => $response['access_token'],
'refresh_token' => $response['refresh_token'] ?? $creds['refresh_token'],
'expires_at' => date('c', time() + ($response['expires_in'] ?? 3600)),
]);
$target->updateCredentials(\KOSMOS\Studio\Support\Encrypter::encrypt(
wp_json_encode($newCreds)
));
return $newCreds['access_token'];
}
}
๐ ุงูุฌุฒุก 5: REST API Endpoints ุงููุงู ูุฉ
Base URL: /wp-json/kosmos/v1/
Convention:
- Auth:ย
Authorization: Bearer <jwt>ย ุฃู WP Nonce - Tenant: ููุญุฏุฏ ุชููุงุฆูุงู ู ู ุงููuser
- Rate limit: 100 req/min/tenant
- Response: JSON ู ูุญุฏ
Response Envelope:
json
{
"success": true,
"data": { ... },
"meta": { "page": 1, "total": 100 }
}
json
{
"success": false,
"error": {
"code": "entitlement_exhausted",
"message": "Your plan has reached the limit for products",
"details": { "feature": "products.sync", "limit": 500, "used": 500 }
}
}
๐ ุฌุฏูู Endpoints ุงููุงู ู:
| Method | Endpoint | Description | Auth | Entitlement |
|---|---|---|---|---|
| Auth | ||||
| POST | /auth/register | ุชุณุฌูู tenant ุฌุฏูุฏ | โ | โ |
| POST | /auth/login | ุชุณุฌูู ุฏุฎูู | โ | โ |
| POST | /auth/refresh | ุชุญุฏูุซ JWT | โ | โ |
| GET | /auth/me | ู ุนููู ุงุช ุงูู ุณุชุฎุฏู | โ | โ |
| Tenants | ||||
| GET | /tenant | ุจูุงูุงุช ุงููtenant ุงูุญุงูู | โ | โ |
| PATCH | /tenant | ุชุญุฏูุซ ุงูุจูุงูุงุช | โ | โ |
| GET | /tenant/usage | ุงุณุชููุงู ุงูุญุตุต | โ | โ |
| GET | /tenant/limits | ุงูุญุฏูุฏ ุงูุญุงููุฉ | โ | โ |
| Subscriptions | ||||
| GET | /plans | ูุงุฆู ุฉ ุงูุจุงูุงุช | โ | โ |
| GET | /subscription | ุงุดุชุฑุงูู ุงูุญุงูู | โ | โ |
| POST | /subscription/checkout | ุฅูุดุงุก ุฌูุณุฉ ุฏูุน | โ | โ |
| POST | /subscription/cancel | ุฅูุบุงุก | โ | โ |
| POST | /subscription/resume | ุงุณุชุฆูุงู | โ | โ |
| POST | /subscription/upgrade | ุชุฑููุฉ | โ | โ |
| Sources | ||||
| GET | /sources | ูุงุฆู ุฉ ุงูู ุตุงุฏุฑ | โ | sources.list |
| POST | /sources | ุฅุถุงูุฉ ู ุตุฏุฑ | โ | sources.create |
| GET | /sources/{id} | ุชูุงุตูู ู ุตุฏุฑ | โ | sources.view |
| PATCH | /sources/{id} | ุชุญุฏูุซ | โ | sources.update |
| DELETE | /sources/{id} | ุญุฐู | โ | sources.delete |
| POST | /sources/{id}/test | ุงุฎุชุจุงุฑ ุงูุงุชุตุงู | โ | sources.test |
| POST | /sources/{id}/sync | ุจุฏุก sync ูุฏูู | โ | sources.sync |
| GET | /sources/{id}/status | ุญุงูุฉ ุงููsync | โ | sources.view |
| Targets | ||||
| GET | /targets | ูุงุฆู ุฉ ุงูุฃูุฏุงู | โ | targets.list |
| POST | /targets | ุฅุถุงูุฉ ูุฏู (ุณูุฉ) | โ | targets.create |
| POST | /targets/salla/oauth | OAuth ู ุน ุณูุฉ | โ | targets.create |
| GET | /targets/{id} | ุชูุงุตูู | โ | targets.view |
| DELETE | /targets/{id} | ุญุฐู | โ | targets.delete |
| POST | /targets/{id}/test | ุงุฎุชุจุงุฑ | โ | targets.test |
| POST | /targets/{id}/publish-all | ูุดุฑ ุงููู | โ | targets.publish |
| Products | ||||
| GET | /products | ูุงุฆู ุฉ ุงูู ูุชุฌุงุช | โ | products.list |
| GET | /products/{id} | ุชูุงุตูู | โ | products.view |
| POST | /products | ุฅูุดุงุก ูุฏูู | โ | products.create |
| PATCH | /products/{id} | ุชุญุฏูุซ | โ | products.update |
| DELETE | /products/{id} | ุญุฐู | โ | products.delete |
| POST | /products/{id}/publish | ูุดุฑ ูู ูุตุฉ | โ | products.publish |
| POST | /products/bulk-publish | ูุดุฑ ุฌู ุงุนู | โ | products.bulk_publish |
| GET | /products/{id}/lock-status | ๐ ุญุงูุฉ ุงูููู | โ | products.view |
| AI | ||||
| POST | /ai/tutor | ู ุญุงุฏุซุฉ AI | โ | ai.tutor |
| POST | /ai/enrich | ุชุญุณูู ู ูุชุฌ | โ | ai.enrich |
| POST | /ai/translate | ุชุฑุฌู ุฉ | โ | ai.translate |
| POST | /ai/generate-description | ุชูููุฏ ูุตู | โ | ai.generate |
| GET | /ai/usage | ุงุณุชููุงู AI | โ | โ |
| Webhooks | ||||
| POST | /webhooks/salla | webhooks ู ู ุณูุฉ | signature | โ |
| POST | /webhooks/stripe | webhooks ู ู Stripe | signature | โ |
| POST | /webhooks/moyasar | webhooks ู ู Moyasar | signature | โ |
| Admin (requires admin) | ||||
| GET | /admin/tenants | ูู ุงููtenants | โ admin | โ |
| GET | /admin/jobs | ุญุงูุฉ ุงูุทูุงุจูุฑ | โ admin | โ |
| POST | /admin/jobs/{id}/retry | ุฅุนุงุฏุฉ ู ุญุงููุฉ | โ admin | โ |
| GET | /admin/audit | ุณุฌู ุงูุชุฏููู | โ admin | โ |
| Health | ||||
| GET | /health | ุตุญุฉ ุงูุฎุฏู ุฉ | โ | โ |
| GET | /metrics | ู ูุงููุณ Prometheus | โ | โ |
ู ุซุงู ุชุทุจููู โ Sync Endpoint:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Http\Controllers;
use KOSMOS\Studio\Application\Jobs\JobQueue;
use KOSMOS\Studio\Domain\Entitlement\EntitlementService;
final class SourceController {
public function __construct(
private \KOSMOS\Studio\Domain\Source\SourceService $sources,
private EntitlementService $entitlements
) {}
/**
* POST /sources/{id}/sync
*/
public function sync(\WP_REST_Request $req): \WP_REST_Response {
$tenantId = get_current_user_id(); // simplified
$sourceId = (int) $req->get_param('id');
// 1. ุชุญูู ู
ู ุงูุงุดุชุฑุงู
if (!$this->entitlements->can($tenantId, 'sources.sync')) {
return new \WP_REST_Response([
'success' => false,
'error' => [
'code' => 'entitlement_exhausted',
'message' => 'Sync limit reached. Upgrade your plan.',
],
], 402);
}
// 2. ุชุญูู ู
ู ุญุฏ ุงูู
ุตุงุฏุฑ
$source = $this->sources->find($sourceId, $tenantId);
if (!$source) {
return new \WP_REST_Response([
'success' => false,
'error' => ['code' => 'not_found'],
], 404);
}
// 3. ุงุฏูุน job
$jobId = JobQueue::push($tenantId, 'sync_source', [
'source_id' => $sourceId,
'mode' => $req->get_param('mode') ?? 'full',
]);
// 4. ุงุณุชููู ู
ู ุงูุญุตุฉ
$this->entitlements->consume($tenantId, 'sources.sync');
return new \WP_REST_Response([
'success' => true,
'data' => [
'job_id' => $jobId,
'status' => 'queued',
'poll_url' => "/wp-json/kosmos/v1/jobs/{$jobId}",
],
], 202);
}
}
๐ ุงูุฌุฒุก 6: Roles & Capabilities
Custom Roles:
php
<?php
// ูููููุฐ ุนูุฏ activation
function kosmos_register_roles(): void {
// 1. Platform Owner โ ู
ุงูู ุงูู
ูุตุฉ
add_role('kosmos_owner', 'KOSMOS Owner', [
'read' => true,
'kosmos_manage_platform' => true,
'kosmos_manage_tenants' => true,
'kosmos_manage_plans' => true,
'kosmos_view_audit' => true,
'kosmos_manage_workers' => true,
]);
// 2. Tenant Admin โ ู
ุฏูุฑ ุญุณุงุจ
add_role('kosmos_tenant_admin', 'Tenant Admin', [
'read' => true,
'kosmos_manage_tenant' => true,
'kosmos_manage_sources' => true,
'kosmos_manage_targets' => true,
'kosmos_manage_products' => true,
'kosmos_manage_billing' => true,
'kosmos_use_ai' => true,
'kosmos_invite_users' => true,
]);
// 3. Tenant Editor โ ู
ุญุฑุฑ
add_role('kosmos_tenant_editor', 'Tenant Editor', [
'read' => true,
'kosmos_manage_sources' => true,
'kosmos_manage_products' => true,
'kosmos_use_ai' => true,
]);
// 4. Tenant Viewer โ ู
ุดุงูุฏ ููุท
add_role('kosmos_tenant_viewer', 'Tenant Viewer', [
'read' => true,
'kosmos_view_products' => true,
]);
// 5. API Consumer โ ูุตูู API ููุท
add_role('kosmos_api', 'API Consumer', [
'read' => true,
'kosmos_api_access' => true,
]);
}
Capabilities ุงูู ุฎุตุตุฉ:
php
<?php
// ูููููุฐ ุนูุฏ activation
function kosmos_register_capabilities(): void {
$caps = [
// Platform-level
'kosmos_manage_platform',
'kosmos_manage_tenants',
'kosmos_manage_plans',
'kosmos_view_audit',
'kosmos_manage_workers',
// Tenant-level
'kosmos_manage_tenant',
'kosmos_manage_billing',
'kosmos_invite_users',
// Sources
'kosmos_manage_sources',
'kosmos_sync_sources',
// Targets
'kosmos_manage_targets',
'kosmos_publish_products',
// Products
'kosmos_manage_products',
'kosmos_view_products',
'kosmos_bulk_publish',
// AI
'kosmos_use_ai',
'kosmos_manage_ai_settings',
// API
'kosmos_api_access',
];
// ุงู
ูุญ ูู ุงูุตูุงุญูุงุช ููู
ุฏูุฑ ุงูุนุงู
$admin = get_role('administrator');
foreach ($caps as $cap) {
$admin->add_cap($cap);
}
}
ู ุตูููุฉ ุงูุตูุงุญูุงุช:
| Capability | Owner | Tenant Admin | Editor | Viewer | API |
|---|---|---|---|---|---|
kosmos_manage_platform | โ | โ | โ | โ | โ |
kosmos_manage_tenants | โ | โ | โ | โ | โ |
kosmos_manage_tenant | โ | โ | โ | โ | โ |
kosmos_manage_billing | โ | โ | โ | โ | โ |
kosmos_manage_sources | โ | โ | โ | โ | โ |
kosmos_manage_targets | โ | โ | โ | โ | โ |
kosmos_publish_products | โ | โ | โ | โ | โ |
kosmos_manage_products | โ | โ | โ | โ | โ |
kosmos_view_products | โ | โ | โ | โ | โ |
kosmos_use_ai | โ | โ | โ | โ | โ |
kosmos_api_access | โ | โ | โ | โ | โ |
๐ณ ุงูุฌุฒุก 7: ูุธุงู ุงูุจุงูุงุช ูุงููEntitlements
ุงูุจุงูุงุช (Plans):
php
<?php
// config/plans.php
return [
'trial' => [
'name' => 'ุชุฌุฑูุจู',
'description' => '14 ููู
ุงู ูุชุฌุฑุจุฉ ูู ุงูู
ูุฒุงุช',
'price_monthly' => 0,
'price_yearly' => 0,
'trial_days' => 14,
'limits' => [
'products' => 50,
'sources' => 1,
'targets' => 1,
'users' => 1,
'ai_credits' => 100,
'storage_mb' => 256,
],
'features' => [
'salla_integration' => true,
'shopify_integration' => false,
'ai_tutor' => true,
'knowledge_graph' => true,
'white_label' => false,
'api_access' => false,
'priority_support' => false,
'custom_domain' => false,
],
],
'starter' => [
'name' => 'ุงูู
ุจุชุฏุฆ',
'description' => 'ููุชุฌุงุฑ ุงูุฃูุฑุงุฏ',
'price_monthly' => 99,
'price_yearly' => 990,
'trial_days' => 7,
'limits' => [
'products' => 500,
'sources' => 2,
'targets' => 1,
'users' => 1,
'ai_credits' => 1000,
'storage_mb' => 1024,
],
'features' => [
'salla_integration' => true,
'shopify_integration' => true,
'ai_tutor' => true,
'knowledge_graph' => true,
'white_label' => false,
'api_access' => false,
'priority_support' => false,
'custom_domain' => false,
],
],
'professional' => [
'name' => 'ุงูุงุญุชุฑุงูู',
'description' => 'ููููุงูุงุช ุงูุตุบูุฑุฉ',
'price_monthly' => 299,
'price_yearly' => 2990,
'trial_days' => 7,
'limits' => [
'products' => 5000,
'sources' => 5,
'targets' => 3,
'users' => 5,
'ai_credits' => 5000,
'storage_mb' => 10240,
],
'features' => [
'salla_integration' => true,
'shopify_integration' => true,
'ai_tutor' => true,
'knowledge_graph' => true,
'white_label' => false,
'api_access' => true,
'priority_support' => true,
'custom_domain' => true,
],
],
'enterprise' => [
'name' => 'ุงูู
ุคุณุณู',
'description' => 'ููุดุฑูุงุช ุงููุจูุฑุฉ',
'price_monthly' => 999,
'price_yearly' => 9990,
'trial_days' => 14,
'limits' => [
'products' => -1, // unlimited
'sources' => -1,
'targets' => -1,
'users' => -1,
'ai_credits' => 50000,
'storage_mb' => -1,
],
'features' => [
'salla_integration' => true,
'shopify_integration' => true,
'ai_tutor' => true,
'knowledge_graph' => true,
'white_label' => true,
'api_access' => true,
'priority_support' => true,
'custom_domain' => true,
'sla' => true,
'dedicated_support' => true,
],
],
];
Feature Keys:
php
<?php
// config/features.php
return [
// Quantifiable
'products.sync' => ['type' => 'quota', 'unit' => 'product'],
'products.publish' => ['type' => 'quota', 'unit' => 'product'],
'sources.create' => ['type' => 'quota', 'unit' => 'source'],
'sources.sync' => ['type' => 'quota', 'unit' => 'sync'],
'targets.create' => ['type' => 'quota', 'unit' => 'target'],
'users.invite' => ['type' => 'quota', 'unit' => 'user'],
'ai.credits' => ['type' => 'metered', 'unit' => 'credit'],
'storage.bytes' => ['type' => 'quota', 'unit' => 'mb'],
// Boolean
'salla_integration' => ['type' => 'boolean'],
'shopify_integration' => ['type' => 'boolean'],
'ai_tutor' => ['type' => 'boolean'],
'knowledge_graph' => ['type' => 'boolean'],
'white_label' => ['type' => 'boolean'],
'api_access' => ['type' => 'boolean'],
'custom_domain' => ['type' => 'boolean'],
'priority_support' => ['type' => 'boolean'],
];
Entitlement Middleware:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Http\Middleware;
use KOSMOS\Studio\Domain\Entitlement\EntitlementService;
/**
* ูู
ูุน ุงููุตูู ูููendpoints ุจุฏูู ุตูุงุญูุฉ
* ููุณุชุฎุฏู
ุนุจุฑ: ->middleware('entitlement:sources.sync')
*/
final class EntitlementMiddleware {
public function __construct(
private EntitlementService $entitlements
) {}
public function handle(\WP_REST_Request $req, string $featureKey): bool|\WP_Error {
$tenantId = get_current_user_id();
if (!$this->entitlements->can($tenantId, $featureKey)) {
return new \WP_Error(
'entitlement_exhausted',
'Your subscription does not allow this action',
[
'status' => 402,
'feature' => $featureKey,
'limits' => $this->entitlements->getLimits($tenantId),
'upgrade' => '/pricing',
]
);
}
return true;
}
}
๐ ุงูุฌุฒุก 8: WooCommerce Hooks Integration
Hooks ุงูู ุณุชู ุน ุฅูููุง:
php
<?php
// src/Integrations/WooCommerce/Hooks.php
declare(strict_types=1);
namespace KOSMOS\Studio\Integrations\WooCommerce;
use KOSMOS\Studio\Application\Bridge\LockOrchestrator;
use KOSMOS\Studio\Domain\Subscription\SubscriptionService;
final class Hooks {
public function __construct(
private SubscriptionService $subscriptions,
private LockOrchestrator $locker
) {}
public function register(): void {
// ===== Subscription lifecycle =====
add_action('woocommerce_subscription_status_active',
[$this, 'onActive'], 10, 1);
add_action('woocommerce_subscription_status_pending-cancel',
[$this, 'onPendingCancel'], 10, 1);
add_action('woocommerce_subscription_status_cancelled',
[$this, 'onCancelled'], 10, 1);
add_action('woocommerce_subscription_status_expired',
[$this, 'onExpired'], 10, 1);
add_action('woocommerce_subscription_status_on-hold',
[$this, 'onHold'], 10, 1);
// ===== Renewal =====
add_action('woocommerce_subscription_renewal_payment_complete',
[$this, 'onRenewalComplete'], 10, 2);
add_action('woocommerce_subscription_renewal_payment_failed',
[$this, 'onRenewalFailed'], 10, 2);
// ===== Order =====
add_action('woocommerce_order_status_completed',
[$this, 'onOrderCompleted'], 10, 1);
add_action('woocommerce_order_refunded',
[$this, 'onRefunded'], 10, 2);
// ===== Product =====
add_action('woocommerce_product_set_stock',
[$this, 'onStockChange'], 10, 1);
// ===== Cron =====
add_action('kosmos/check_expirations', [$this, 'checkExpirations']);
}
public function onActive($subscription): void {
$tenantId = $this->resolveTenantId($subscription);
$this->subscriptions->markActive($subscription->get_id());
$this->locker->handleSubscriptionRenewed($tenantId);
}
public function onExpired($subscription): void {
$tenantId = $this->resolveTenantId($subscription);
$this->subscriptions->markExpired($subscription->get_id());
$this->locker->handleSubscriptionExpired($tenantId);
}
public function onCancelled($subscription): void {
// ูุง ูููู ููุฑุงู โ ููุชุธุฑ ุงูุชูุงุก ุงููุชุฑุฉ ุงูู
ุฏููุนุฉ
$tenantId = $this->resolveTenantId($subscription);
$this->subscriptions->markCancelled($subscription->get_id());
// ุฌุฏูู ูุญุต ุจุนุฏ ุงูุชูุงุก ุงููุชุฑุฉ
wp_schedule_single_event(
$subscription->get_date('end')->getTimestamp() + 3600,
'kosmos/check_expirations',
[$tenantId]
);
}
public function onHold($subscription): void {
$tenantId = $this->resolveTenantId($subscription);
// ุงู
ูุญ 3 ุฃูุงู
grace period
$graceEnds = time() + (3 * DAY_IN_SECONDS);
$this->subscriptions->setGracePeriod($subscription->get_id(), $graceEnds);
}
public function onRenewalFailed($subscription, $lastOrder): void {
$tenantId = $this->resolveTenantId($subscription);
$this->subscriptions->incrementFailedPayments($subscription->get_id());
$fails = $this->subscriptions->getFailedCount($subscription->get_id());
// 3 ู
ุญุงููุงุช โ ุซู
ุงูููู
if ($fails >= 3) {
$this->locker->handleSubscriptionExpired($tenantId);
}
}
public function checkExpirations(?int $tenantId = null): void {
if ($tenantId) {
$sub = $this->subscriptions->getActiveByTenant($tenantId);
if ($sub && $sub->current_period_end < time()) {
$this->locker->handleSubscriptionExpired($tenantId);
}
return;
}
// ูุญุต ุฌู
ุงุนู
foreach ($this->subscriptions->getAllExpiring() as $sub) {
$this->locker->handleSubscriptionExpired($sub->tenant_id);
}
}
private function resolveTenantId($subscription): int {
$userId = $subscription->get_user_id();
return (int) get_user_meta($userId, '_kosmos_tenant_id', true);
}
}
Cron Schedule ู ุฎุตุต:
php
<?php
// ุฅุถุงูุฉ ูุชุฑุงุช cron ู
ุฎุตุตุฉ
add_filter('cron_schedules', function ($schedules) {
$schedules['every_minute'] = [
'interval' => 60,
'display' => 'Every Minute',
];
$schedules['every_five_minutes'] = [
'interval' => 300,
'display' => 'Every 5 Minutes',
];
return $schedules;
});
// ุชุณุฌูู ุฃุญุฏุงุซ cron
add_action('init', function () {
if (!wp_next_scheduled('kosmos/check_expirations')) {
wp_schedule_event(time(), 'hourly', 'kosmos/check_expirations');
}
if (!wp_next_scheduled('kosmos/worker_tick')) {
wp_schedule_event(time(), 'every_minute', 'kosmos/worker_tick');
}
});
๐ค ุงูุฌุฒุก 9: AI Gateway ุงููุงู ู
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Application\AI;
use KOSMOS\Studio\Application\AI\Providers\ProviderInterface;
use KOSMOS\Studio\Domain\Entitlement\EntitlementService;
use KOSMOS\Studio\Support\Logger;
/**
* ููุทุฉ ุฏุฎูู ูุงุญุฏุฉ ููู ุงุณุชุฏุนุงุกุงุช AI
*/
final class AIGateway {
/** @var array<string,ProviderInterface> */
private array $providers = [];
public function __construct(
private EntitlementService $entitlements,
private Logger $logger
) {}
public function registerProvider(string $name, ProviderInterface $provider): void {
$this->providers[$name] = $provider;
}
/**
* @param string $feature tutor|enrich|translate|generate
*/
public function call(
int $tenantId,
string $feature,
array $payload,
?string $preferredProvider = null
): array {
// 1. ุชุญูู ู
ู ุงูุตูุงุญูุฉ
$featureKey = "ai.{$feature}";
if (!$this->entitlements->can($tenantId, $featureKey)) {
return [
'success' => false,
'error' => [
'code' => 'entitlement_exhausted',
'message' => 'AI credits exhausted. Please upgrade.',
],
];
}
// 2. ุงุฎุชุฑ ุงูู
ุฒูุฏ
$providerName = $preferredProvider ?? $this->selectProvider($feature);
$provider = $this->providers[$providerName] ?? null;
if (!$provider) {
return [
'success' => false,
'error' => ['code' => 'provider_unavailable'],
];
}
// 3. ูููุฐ
$start = microtime(true);
try {
$result = $provider->execute($feature, $payload);
$duration = (int) ((microtime(true) - $start) * 1000);
// 4. ุงุณุชููู
$credits = $this->calculateCredits($result['tokens'] ?? 0);
$this->entitlements->consume($tenantId, $featureKey, $credits);
// 5. ุณุฌูู
$this->logUsage($tenantId, $feature, $providerName, $result, $duration);
return [
'success' => true,
'data' => $result['data'] ?? $result,
'meta' => [
'provider' => $providerName,
'tokens' => $result['tokens'] ?? 0,
'credits_used' => $credits,
'duration_ms' => $duration,
],
];
} catch (\Throwable $e) {
$this->logger->error('AI call failed', [
'feature' => $feature,
'provider' => $providerName,
'error' => $e->getMessage(),
]);
return [
'success' => false,
'error' => [
'code' => 'ai_provider_error',
'message' => $e->getMessage(),
],
];
}
}
private function selectProvider(string $feature): string {
// ูู
ูู ุฃู ูููู ุฐููุงู: ุญุณุจ ุงูุชูููุฉ/ุงูุณุฑุนุฉ/ุงูุฌูุฏุฉ
$map = [
'tutor' => 'openai',
'enrich' => 'openai',
'translate' => 'google',
'generate' => 'openai',
];
return $map[$feature] ?? 'openai';
}
private function calculateCredits(int $tokens): float {
// 1000 tokens = 1 credit
return round($tokens / 1000, 4);
}
private function logUsage(int $tenantId, string $feature, string $provider, array $result, int $duration): void {
global $wpdb;
$wpdb->insert("{$wpdb->prefix}kosmos_ai_usage", [
'tenant_id' => $tenantId,
'feature' => $feature,
'model' => $result['model'] ?? 'unknown',
'provider' => $provider,
'tokens_in' => $result['tokens_in'] ?? 0,
'tokens_out' => $result['tokens_out'] ?? 0,
'credits_used' => $this->calculateCredits($result['tokens'] ?? 0),
'cost_usd' => $result['cost'] ?? 0,
'duration_ms' => $duration,
'success' => 1,
]);
}
}
Provider Interface + OpenAI:
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Application\AI\Providers;
interface ProviderInterface {
public function execute(string $feature, array $payload): array;
public function name(): string;
}
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Application\AI\Providers;
use OpenAI\Client;
final class OpenAIProvider implements ProviderInterface {
private Client $client;
public function __construct() {
$apiKey = get_option('kosmos_openai_key') ?: getenv('OPENAI_API_KEY');
$this->client = \OpenAI::client($apiKey);
}
public function name(): string {
return 'openai';
}
public function execute(string $feature, array $payload): array {
return match($feature) {
'tutor' => $this->tutor($payload),
'enrich' => $this->enrich($payload),
'generate' => $this->generate($payload),
default => throw new \InvalidArgumentException("Unknown feature: {$feature}"),
};
}
private function tutor(array $payload): array {
$messages = [
['role' => 'system', 'content' => 'ุฃูุช KOSMOS Tutor...'],
];
if (!empty($payload['context'])) {
$messages[] = ['role' => 'system', 'content' => "ุงูุณูุงู:\n{$payload['context']}"];
}
foreach ($payload['history'] ?? [] as $msg) {
$messages[] = $msg;
}
$messages[] = ['role' => 'user', 'content' => $payload['query']];
$response = $this->client->chat()->create([
'model' => 'gpt-4o-mini',
'messages' => $messages,
'temperature' => 0.3,
'max_tokens' => 900,
]);
return [
'data' => $response->choices[0]->message->content,
'tokens' => $response->usage->totalTokens,
'tokens_in' => $response->usage->promptTokens,
'tokens_out' => $response->usage->completionTokens,
'model' => 'gpt-4o-mini',
'cost' => $this->calculateCost($response->usage),
];
}
private function enrich(array $payload): array {
$product = $payload['product'] ?? [];
$response = $this->client->chat()->create([
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'system', 'content' => 'ุฃูุช ุฎุจูุฑ ุชุฌุงุฑุฉ ุฅููุชุฑูููุฉ. ุญุณูู ุจูุงูุงุช ุงูู
ูุชุฌ.'],
['role' => 'user', 'content' => wp_json_encode($product)],
],
'response_format' => ['type' => 'json_object'],
]);
return [
'data' => json_decode($response->choices[0]->message->content, true),
'tokens' => $response->usage->totalTokens,
'model' => 'gpt-4o-mini',
];
}
private function generate(array $payload): array {
$response = $this->client->chat()->create([
'model' => 'gpt-4o-mini',
'messages' => [
['role' => 'user', 'content' => $payload['prompt'] ?? ''],
],
]);
return [
'data' => $response->choices[0]->message->content,
'tokens' => $response->usage->totalTokens,
'model' => 'gpt-4o-mini',
];
}
private function calculateCost(object $usage): float {
// $0.15/1M input, $0.60/1M output (gpt-4o-mini)
return ($usage->promptTokens * 0.15 + $usage->completionTokens * 0.60) / 1_000_000;
}
}
๐ ุงูุฌุฒุก 10: ุงููDashboards
1๏ธโฃ Tenant Dashboard (Frontend):
php
<?php
// src/Frontend/Shortcodes/DashboardShortcode.php
declare(strict_types=1);
namespace KOSMOS\Studio\Frontend\Shortcodes;
final class DashboardShortcode {
public static function render(): string {
if (!is_user_logged_in()) {
return '<p>ูุฌุจ ุชุณุฌูู ุงูุฏุฎูู.</p>';
}
$tenantId = (int) get_user_meta(get_current_user_id(), '_kosmos_tenant_id', true);
$tenant = \KOSMOS\Studio\Domain\Tenant\TenantRepository::find($tenantId);
if (!$tenant) {
return '<p>ูุง ููุฌุฏ ุญุณุงุจ ู
ุฑุชุจุท.</p>';
}
ob_start();
?>
<div class="kosmos-dashboard" data-tenant="<?= esc_attr($tenantId) ?>">
<header class="kosmos-header">
<h1><?= esc_html($tenant->name) ?></h1>
<div class="kosmos-sub-status <?= esc_attr($tenant->status) ?>">
<?php if ($tenant->status === 'active'): ?>
โ
ุงุดุชุฑุงู ูุดุท โ ูุชุฌุฏุฏ ูู <?= esc_html($tenant->current_period_end) ?>
<?php else: ?>
โ ๏ธ <?= esc_html($tenant->status) ?> โ <a href="/pricing">ุฌุฏูุฏ ุงูุขู</a>
<?php endif; ?>
</div>
</header>
<div class="kosmos-grid">
<?php
$stats = [
['๐ฆ', 'ุงูู
ูุชุฌุงุช', \KOSMOS\Studio\Domain\Product\ProductRepository::countByTenant($tenantId)],
['๐', 'ุงูู
ุตุงุฏุฑ', \KOSMOS\Studio\Domain\Source\SourceRepository::countByTenant($tenantId)],
['๐ฏ', 'ูููุงุช ุงูุจูุน', \KOSMOS\Studio\Domain\Target\TargetRepository::countByTenant($tenantId)],
['๐ง ', 'ุงุณุชููุงู AI', self::getAiUsage($tenantId)],
];
foreach ($stats as [$icon, $label, $value]): ?>
<div class="kosmos-card">
<div class="icon"><?= $icon ?></div>
<div class="value"><?= esc_html((string) $value) ?></div>
<div class="label"><?= esc_html($label) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="kosmos-actions">
<a href="/dashboard/sources" class="btn">ุฅุฏุงุฑุฉ ุงูู
ุตุงุฏุฑ</a>
<a href="/dashboard/targets" class="btn">ูููุงุช ุงูุจูุน</a>
<a href="/dashboard/products" class="btn">ุงูู
ูุชุฌุงุช</a>
<a href="/dashboard/tutor" class="btn">ุงุณุฃู AI</a>
</div>
</div>
<?php
return ob_get_clean();
}
private static function getAiUsage(int $tenantId): string {
global $wpdb;
$sum = $wpdb->get_var($wpdb->prepare(
"SELECT SUM(credits_used) FROM {$wpdb->prefix}kosmos_ai_usage
WHERE tenant_id = %d AND created_at >= %s",
$tenantId,
date('Y-m-01')
));
return number_format((float) $sum, 1);
}
}
2๏ธโฃ Admin Dashboard (WP Admin):
php
<?php
// src/Admin/Pages/Dashboard.php
declare(strict_types=1);
namespace KOSMOS\Studio\Admin\Pages;
final class Dashboard {
public function render(): void {
global $wpdb;
$stats = [
'tenants' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}kosmos_tenants WHERE status='active'"),
'mrr' => (float) $wpdb->get_var("SELECT SUM(amount) FROM {$wpdb->prefix}kosmos_subscriptions WHERE status='active' AND billing_cycle='monthly'"),
'products' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}kosmos_products"),
'locked' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}kosmos_products WHERE locked_by_subscription=1"),
'ai_calls_24h' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}kosmos_ai_usage WHERE created_at >= DATE_SUB(NOW(), INTERVAL 24 HOUR)"),
'jobs_queued' => (int) $wpdb->get_var("SELECT COUNT(*) FROM {$wpdb->prefix}kosmos_jobs WHERE status='queued'"),
];
?>
<div class="wrap kosmos-admin-dashboard">
<h1>๐ KOSMOS Control Center</h1>
<div class="kosmos-kpi-grid">
<?php foreach ($stats as $key => $value): ?>
<div class="kpi-card">
<div class="kpi-value"><?= esc_html((string) $value) ?></div>
<div class="kpi-label"><?= esc_html($key) ?></div>
</div>
<?php endforeach; ?>
</div>
<div class="kosmos-panels">
<div class="panel">
<h2>ุงุดุชุฑุงูุงุช ุชูุชูู ูุฑูุจุงู</h2>
<?php $this->renderExpiringSubscriptions(); ?>
</div>
<div class="panel">
<h2>ู
ูุงู
ููุฏ ุงูุชูููุฐ</h2>
<?php $this->renderRunningJobs(); ?>
</div>
<div class="panel">
<h2>ุฃุฎุทุงุก ุญุฏูุซุฉ</h2>
<?php $this->renderRecentErrors(); ?>
</div>
</div>
</div>
<?php
}
private function renderExpiringSubscriptions(): void {
global $wpdb;
$rows = $wpdb->get_results("
SELECT s.id, t.name, s.current_period_end
FROM {$wpdb->prefix}kosmos_subscriptions s
JOIN {$wpdb->prefix}kosmos_tenants t ON t.id = s.tenant_id
WHERE s.status = 'active'
AND s.current_period_end <= DATE_ADD(NOW(), INTERVAL 7 DAY)
ORDER BY s.current_period_end ASC
LIMIT 20
");
if (!$rows) {
echo '<p>ูุง ููุฌุฏ.</p>';
return;
}
echo '<table class="widefat"><thead><tr>';
echo '<th>Tenant</th><th>ููุชูู ูู</th><th>ุงูุญุงูุฉ</th>';
echo '</tr></thead><tbody>';
foreach ($rows as $r) {
$days = (strtotime($r->current_period_end) - time()) / 86400;
echo '<tr>';
echo '<td>' . esc_html($r->name) . '</td>';
echo '<td>' . esc_html($r->current_period_end) . '</td>';
echo '<td>' . ($days < 0 ? 'โ ๏ธ ู
ูุชูู' : sprintf('ุฎูุงู %.1f ููู
', $days)) . '</td>';
echo '</tr>';
}
echo '</tbody></table>';
}
private function renderRunningJobs(): void {
global $wpdb;
$rows = $wpdb->get_results("
SELECT id, job_type, tenant_id, status, attempts, started_at
FROM {$wpdb->prefix}kosmos_jobs
WHERE status IN ('queued','running')
ORDER BY priority ASC, scheduled_at ASC
LIMIT 20
");
if (!$rows) {
echo '<p>ูุง ููุฌุฏ ู
ูุงู
ููุฏ ุงูุชูููุฐ.</p>';
return;
}
echo '<table class="widefat"><thead><tr>';
echo '<th>#</th><th>ุงูููุน</th><th>Tenant</th><th>ุงูุญุงูุฉ</th><th>ุงูู
ุญุงููุงุช</th>';
echo '</tr></thead><tbody>';
foreach ($rows as $r) {
echo '<tr>';
echo '<td>' . (int) $r->id . '</td>';
echo '<td>' . esc_html($r->job_type) . '</td>';
echo '<td>' . (int) $r->tenant_id . '</td>';
echo '<td>' . esc_html($r->status) . '</td>';
echo '<td>' . (int) $r->attempts . '</td>';
echo '</tr>';
}
echo '</tbody></table>';
}
private function renderRecentErrors(): void {
global $wpdb;
$rows = $wpdb->get_results("
SELECT id, source, event_type, error_message, created_at
FROM {$wpdb->prefix}kosmos_webhook_events
WHERE error_message IS NOT NULL
ORDER BY created_at DESC
LIMIT 10
");
if (!$rows) {
echo '<p>ูุง ููุฌุฏ ุฃุฎุทุงุก.</p>';
return;
}
echo '<ul>';
foreach ($rows as $r) {
echo '<li>';
echo '<strong>' . esc_html($r->source) . '/' . esc_html($r->event_type) . '</strong>: ';
echo esc_html(substr($r->error_message, 0, 100));
echo ' <em>(' . esc_html($r->created_at) . ')</em>';
echo '</li>';
}
echo '</ul>';
}
}
โก ุงูุฌุฒุก 11: Job Queue & Workers
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Application\Jobs;
final class JobQueue {
public static function push(
int $tenantId,
string $jobType,
array $payload,
int $priority = 5,
?int $delaySeconds = null
): int {
global $wpdb;
$wpdb->insert("{$wpdb->prefix}kosmos_jobs", [
'tenant_id' => $tenantId,
'job_type' => $jobType,
'payload' => wp_json_encode($payload),
'status' => 'queued',
'priority' => $priority,
'scheduled_at' => $delaySeconds
? date('Y-m-d H:i:s', time() + $delaySeconds)
: current_time('mysql'),
'created_at' => current_time('mysql'),
]);
return (int) $wpdb->insert_id;
}
public static function claim(string $workerId): ?array {
global $wpdb;
// ููู ุฐุฑู โ ูู
ูุน ุชูููุฐ ููุณ ุงูู
ูู
ุฉ ู
ุฑุชูู
$wpdb->query('START TRANSACTION');
$job = $wpdb->get_row($wpdb->prepare("
SELECT * FROM {$wpdb->prefix}kosmos_jobs
WHERE status = 'queued'
AND scheduled_at <= NOW()
ORDER BY priority ASC, scheduled_at ASC
LIMIT 1
FOR UPDATE SKIP LOCKED
"));
if (!$job) {
$wpdb->query('COMMIT');
return null;
}
$wpdb->update(
"{$wpdb->prefix}kosmos_jobs",
[
'status' => 'running',
'started_at' => current_time('mysql'),
'worker_id' => $workerId,
'attempts' => $job->attempts + 1,
],
['id' => $job->id]
);
$wpdb->query('COMMIT');
return (array) $job;
}
public static function complete(int $jobId, array $result = []): void {
global $wpdb;
$wpdb->update(
"{$wpdb->prefix}kosmos_jobs",
[
'status' => 'completed',
'completed_at' => current_time('mysql'),
'result' => wp_json_encode($result),
],
['id' => $jobId]
);
}
public static function fail(int $jobId, string $error): void {
global $wpdb;
$job = $wpdb->get_row($wpdb->prepare(
"SELECT attempts, max_attempts FROM {$wpdb->prefix}kosmos_jobs WHERE id = %d",
$jobId
));
$status = ($job && $job->attempts >= $job->max_attempts) ? 'failed' : 'queued';
// Exponential backoff
$delay = min(3600, pow(2, $job->attempts ?? 1) * 30);
$wpdb->update(
"{$wpdb->prefix}kosmos_jobs",
[
'status' => $status,
'error_message' => $error,
'scheduled_at' => date('Y-m-d H:i:s', time() + $delay),
],
['id' => $jobId]
);
}
}
php
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Application\Jobs;
use KOSMOS\Studio\Application\Bridge\SyncOrchestrator;
use KOSMOS\Studio\Application\Bridge\PublishOrchestrator;
use KOSMOS\Studio\Domain\Product\ProductLockManager;
final class Worker {
private const WORKER_ID_PREFIX = 'kosmos-worker-';
private const MAX_JOBS_PER_TICK = 5;
private const TIME_BUDGET_SECONDS = 50;
public static function tick(): void {
$workerId = self::WORKER_ID_PREFIX . gethostname() . '-' . getmypid();
$start = time();
for ($i = 0; $i < self::MAX_JOBS_PER_TICK; $i++) {
if ((time() - $start) > self::TIME_BUDGET_SECONDS) break;
$job = JobQueue::claim($workerId);
if (!$job) break;
try {
$result = self::execute($job);
JobQueue::complete($job['id'], $result);
} catch (\Throwable $e) {
JobQueue::fail($job['id'], $e->getMessage());
}
}
}
private static function execute(array $job): array {
$payload = json_decode($job['payload'], true) ?: [];
$tenantId = (int) $job['tenant_id'];
return match ($job['job_type']) {
'sync_source' => self::handleSyncSource($tenantId, $payload),
'publish_product' => self::handlePublishProduct($tenantId, $payload),
'bulk_lock' => self::handleBulkLock($tenantId, $payload),
default => throw new \RuntimeException("Unknown job: {$job['job_type']}"),
};
}
private static function handleSyncSource(int $tenantId, array $payload): array {
$orchestrator = \KOSMOS\Studio\Core\Plugin::instance()
->container()
->get(SyncOrchestrator::class);
return $orchestrator->syncSource(
$tenantId,
(int) $payload['source_id'],
$payload['mode'] ?? 'full'
);
}
private static function handlePublishProduct(int $tenantId, array $payload): array {
$orchestrator = \KOSMOS\Studio\Core\Plugin::instance()
->container()
->get(PublishOrchestrator::class);
return $orchestrator->publish(
$tenantId,
(int) $payload['product_id'],
(int) $payload['target_id']
);
}
private static function handleBulkLock(int $tenantId, array $payload): array {
$targets = \KOSMOS\Studio\Domain\Target\TargetRepository::find(
(int) $payload['target_id'],
$tenantId
);
if (!$targets) return ['skipped' => true];
$publisher = new \KOSMOS\Studio\Integrations\Salla\SallaPublisher(
new \KOSMOS\Studio\Integrations\Salla\SallaClient(),
new \KOSMOS\Studio\Integrations\Salla\SallaMapper()
);
$action = $payload['action'] ?? 'hide';
$products = \KOSMOS\Studio\Domain\Product\ProductRepository::findByTarget($tenantId, (int) $payload['target_id']);
$done = 0;
foreach ($products as $p) {
if (!$p['external_id']) continue;
$ok = $action === 'hide'
? $publisher->hide($targets, $p['external_id'])
: $publisher->show($targets, $p['external_id']);
if ($ok) $done++;
}
return ['action' => $action, 'affected' => $done];
}
}
๐ ุงูุฌุฒุก 12: ุฎุทุฉ ุงูุชูููุฐ โ ุฃูู 60 ุฏูููุฉ ู ูููุง ุจู ูู
๐ ุงูุฏูููุฉ 0-5 | Bootstrap
bash
# 1. ุฃูุดุฆ ุงูู ุดุฑูุน mkdir -p kosmos-studio && cd kosmos-studio composer init --name=kosmos/studio --type=wordpress-plugin
json
// composer.json
{
"name": "kosmos/studio",
"type": "wordpress-plugin",
"require": {
"php": ">=8.2",
"openai-php/client": "^0.10",
"guzzlehttp/guzzle": "^7.8",
"monolog/monolog": "^3.6",
"ramsey/uuid": "^4.7",
"defuse/php-encryption": "^2.4"
},
"require-dev": {
"phpunit/phpunit": "^11.0",
"mockery/mockery": "^1.6",
"squizlabs/php_codesniffer": "^3.10",
"phpstan/phpstan": "^1.11"
},
"autoload": {
"psr-4": {
"KOSMOS\\Studio\\": "src/"
}
},
"autoload-dev": {
"psr-4": {
"KOSMOS\\Studio\\Tests\\": "tests/"
}
},
"config": {
"optimize-autoloader": true
},
"scripts": {
"test": "phpunit",
"lint": "phpcs --standard=PSR12 src/",
"analyze": "phpstan analyse src/ --level=8"
}
}
bash
composer install
๐ ุงูุฏูููุฉ 5-10 | Plugin Main File
bash
cat > kosmos-studio.php << 'EOF'
<?php
/**
* Plugin Name: KOSMOS Studio
* Description: Subscription-as-a-Service Knowledge & Commerce Bridge
* Version: 1.0.0
* Requires PHP: 8.2
*/
declare(strict_types=1);
namespace KOSMOS\Studio;
if (!defined('ABSPATH')) exit;
require_once __DIR__ . '/vendor/autoload.php';
define('KOSMOS_VERSION', '1.0.0');
define('KOSMOS_FILE', __FILE__);
define('KOSMOS_PATH', plugin_dir_path(__FILE__));
define('KOSMOS_URL', plugin_dir_url(__FILE__));
Core\Plugin::instance()->boot();
EOF
๐ ุงูุฏูููุฉ 10-20 | Core Classes
bash
mkdir -p src/{Core,Database/{Migrations,Seeds},Domain/{Tenant,Subscription,Entitlement,Product,Source,Target},Application/{Bridge,AI/Providers,Jobs/Handlers},Integrations/{Salla,Shopify,Payments},Http/{Middleware,Controllers},Admin/Pages,Frontend/Shortcodes,Support,Exceptions}
bash
# Core/Plugin.php
cat > src/Core/Plugin.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Core;
use KOSMOS\Studio\Database\Migrator;
use KOSMOS\Studio\Http\Router;
final class Plugin {
private static ?self $instance = null;
private Container $container;
public static function instance(): self {
return self::$instance ??= new self();
}
private function __construct() {
$this->container = new Container();
}
public function boot(): void {
register_activation_hook(KOSMOS_FILE, [$this, 'onActivate']);
add_action('plugins_loaded', [$this, 'onPluginsLoaded']);
add_action('init', [$this, 'onInit']);
add_action('rest_api_init', [$this, 'registerRestRoutes']);
}
public function onActivate(): void {
(new Migrator())->run();
(new \KOSMOS\Studio\Database\Seeds\PlansSeeder())->run();
}
public function onPluginsLoaded(): void {
if (!class_exists('WooCommerce')) return;
load_plugin_textdomain('kosmos', false, dirname(plugin_basename(KOSMOS_FILE)) . '/languages');
}
public function onInit(): void {
add_shortcode('kosmos_pricing', [\KOSMOS\Studio\Frontend\Shortcodes\PricingShortcode::class, 'render']);
}
public function registerRestRoutes(): void {
(new Router())->register();
}
public function container(): Container { return $this->container; }
}
EOF
bash
# Core/Container.php
cat > src/Core/Container.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Core;
final class Container {
private array $bindings = [];
private array $instances = [];
public function bind(string $abstract, callable $factory): void {
$this->bindings[$abstract] = $factory;
}
public function singleton(string $abstract, ?callable $factory = null): void {
$factory ??= fn() => new $abstract();
$this->bindings[$abstract] = function () use ($factory, $abstract) {
return $this->instances[$abstract] ??= $factory($this);
};
}
public function get(string $abstract): mixed {
if (isset($this->instances[$abstract])) return $this->instances[$abstract];
$factory = $this->bindings[$abstract] ?? null;
if (!$factory) {
if (!class_exists($abstract)) throw new \RuntimeException("Not bound: {$abstract}");
return $this->instances[$abstract] = new $abstract();
}
return $factory($this);
}
}
EOF
๐ ุงูุฏูููุฉ 20-30 | Database Schema
bash
# Database/Migrator.php
cat > src/Database/Migrator.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Database;
final class Migrator {
public function run(): void {
$migrations = glob(KOSMOS_PATH . 'src/Database/Migrations/*.php');
sort($migrations);
$ran = get_option('kosmos_migrations', []);
foreach ($migrations as $file) {
$name = basename($file, '.php');
if (in_array($name, $ran, true)) continue;
require_once $file;
$class = 'KOSMOS\\Studio\\Database\\Migrations\\' . $name;
if (class_exists($class)) {
(new $class())->up();
$ran[] = $name;
}
}
update_option('kosmos_migrations', $ran);
}
}
EOF
bash
# Migration 001 - Tenants
cat > src/Database/Migrations/Migration_001_CreateTenants.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Database\Migrations;
final class Migration_001_CreateTenants {
public function up(): void {
global $wpdb;
$charset = $wpdb->get_charset_collate();
$wpdb->query("CREATE TABLE IF NOT EXISTS {$wpdb->prefix}kosmos_tenants (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
uuid CHAR(36) NOT NULL UNIQUE,
slug VARCHAR(64) NOT NULL UNIQUE,
name VARCHAR(191) NOT NULL,
owner_user_id BIGINT UNSIGNED NOT NULL,
plan_slug VARCHAR(32) NOT NULL DEFAULT 'trial',
status ENUM('active','suspended','cancelled','trial') DEFAULT 'trial',
subdomain VARCHAR(64) NOT NULL UNIQUE,
custom_domain VARCHAR(191) NULL UNIQUE,
salla_store_id VARCHAR(64) NULL,
salla_access_token TEXT NULL,
salla_refresh_token TEXT NULL,
salla_token_expires DATETIME NULL,
max_products INT DEFAULT 100,
max_users INT DEFAULT 3,
max_sources INT DEFAULT 2,
ai_credits INT DEFAULT 500,
trial_ends_at DATETIME NULL,
current_period_start DATETIME NULL,
current_period_end DATETIME NULL,
metadata JSON NULL,
created_at DATETIME DEFAULT CURRENT_TIMESTAMP,
updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
deleted_at DATETIME NULL,
INDEX idx_status (status),
INDEX idx_plan (plan_slug),
INDEX idx_owner (owner_user_id),
INDEX idx_period_end (current_period_end)
) $charset;");
}
}
EOF
(ููุฑุฑ ููุณ ุงููู ุท ูู 11 migration ู ุชุจููุฉ ุญุณุจ ุงูุฌุฏุงูู ุงูู ูุนุฑููุฉ ูู ุงูุฌุฒุก 2)
๐ ุงูุฏูููุฉ 30-45 | Core Domain Services
bash
# Domain/Entitlement/EntitlementService.php
cat > src/Domain/Entitlement/EntitlementService.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Domain\Entitlement;
final class EntitlementService {
private const CACHE_TTL = 300;
public function can(int $tenantId, string $featureKey, ?int $scopeId = null): bool {
$cacheKey = "entitlement:{$tenantId}:{$featureKey}:{$scopeId}";
$cached = wp_cache_get($cacheKey, 'kosmos');
if ($cached !== false) return (bool) $cached;
global $wpdb;
$row = $wpdb->get_row($wpdb->prepare("
SELECT * FROM {$wpdb->prefix}kosmos_entitlements
WHERE tenant_id = %d AND feature_key = %s
AND (scope_id = %d OR scope_id IS NULL)
AND is_enabled = 1
AND (expires_at IS NULL OR expires_at > NOW())
LIMIT 1
", $tenantId, $featureKey, $scopeId ?? 0));
$allowed = $row && ($row->limit_value === null || $row->used_value < $row->limit_value);
wp_cache_set($cacheKey, $allowed ? 1 : 0, 'kosmos', self::CACHE_TTL);
return (bool) $allowed;
}
public function consume(int $tenantId, string $featureKey, float $amount = 1): bool {
global $wpdb;
$affected = $wpdb->query($wpdb->prepare("
UPDATE {$wpdb->prefix}kosmos_entitlements
SET used_value = used_value + %f, updated_at = NOW()
WHERE tenant_id = %d
AND feature_key = %s
AND is_enabled = 1
AND (limit_value IS NULL OR used_value + %f <= limit_value)
", $amount, $tenantId, $featureKey, $amount));
if ($affected) {
wp_cache_delete("entitlement:{$tenantId}:{$featureKey}:0", 'kosmos');
}
return $affected > 0;
}
public function grant(int $tenantId, string $featureKey, int $limit, ?int $scopeId = null): void {
global $wpdb;
$wpdb->replace("{$wpdb->prefix}kosmos_entitlements", [
'tenant_id' => $tenantId,
'feature_key' => $featureKey,
'scope_type' => 'global',
'scope_id' => $scopeId,
'limit_value' => $limit,
'used_value' => 0,
'reset_period' => 'monthly',
'last_reset_at' => current_time('mysql'),
'is_enabled' => 1,
'created_at' => current_time('mysql'),
'updated_at' => current_time('mysql'),
]);
wp_cache_delete("entitlement:{$tenantId}:{$featureKey}:{$scopeId}", 'kosmos');
}
public function revokeAll(int $tenantId): void {
global $wpdb;
$wpdb->update(
"{$wpdb->prefix}kosmos_entitlements",
['is_enabled' => 0],
['tenant_id' => $tenantId]
);
wp_cache_flush_group('kosmos');
}
public function grantFromPlan(int $tenantId): void {
global $wpdb;
$plan = $wpdb->get_row($wpdb->prepare("
SELECT p.* FROM {$wpdb->prefix}kosmos_plans p
JOIN {$wpdb->prefix}kosmos_tenants t ON t.plan_slug = p.slug
WHERE t.id = %d
", $tenantId));
if (!$plan) return;
$limits = json_decode($plan->limits, true) ?: [];
foreach ($limits as $feature => $value) {
$this->grant($tenantId, $feature, (int) $value);
}
}
}
EOF
bash
# Domain/Product/ProductLockManager.php
cat > src/Domain/Product/ProductLockManager.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Domain\Product;
use KOSMOS\Studio\Application\Jobs\JobQueue;
final class ProductLockManager {
public function lockAll(int $tenantId, string $reason = 'subscription_expired'): int {
global $wpdb;
$count = $wpdb->query($wpdb->prepare("
UPDATE {$wpdb->prefix}kosmos_products
SET locked_by_subscription = 1, sync_status = 'locked', updated_at = NOW()
WHERE tenant_id = %d AND deleted_at IS NULL
", $tenantId));
// ุฃุถู job ูุฅุฎูุงุก ุงูู
ูุชุฌุงุช ู
ู ุณูุฉ
$targets = $wpdb->get_results($wpdb->prepare("
SELECT id FROM {$wpdb->prefix}kosmos_targets
WHERE tenant_id = %d AND deleted_at IS NULL
", $tenantId));
foreach ($targets as $t) {
JobQueue::push($tenantId, 'bulk_lock', [
'target_id' => (int) $t->id,
'action' => 'hide',
'reason' => $reason,
], priority: 1);
}
do_action('kosmos/products_locked', $tenantId, $count, $reason);
return (int) $count;
}
public function unlockAll(int $tenantId): int {
global $wpdb;
$count = $wpdb->query($wpdb->prepare("
UPDATE {$wpdb->prefix}kosmos_products
SET locked_by_subscription = 0, sync_status = 'outdated', updated_at = NOW()
WHERE tenant_id = %d AND locked_by_subscription = 1
", $tenantId));
$targets = $wpdb->get_results($wpdb->prepare("
SELECT id FROM {$wpdb->prefix}kosmos_targets
WHERE tenant_id = %d AND is_locked = 1
", $tenantId));
foreach ($targets as $t) {
JobQueue::push($tenantId, 'bulk_lock', [
'target_id' => (int) $t->id,
'action' => 'show',
], priority: 1);
}
do_action('kosmos/products_unlocked', $tenantId, $count);
return (int) $count;
}
}
EOF
๐ ุงูุฏูููุฉ 45-55 | Salla Integration
bash
# Integrations/Salla/SallaPublisher.php
cat > src/Integrations/Salla/SallaPublisher.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Integrations\Salla;
final class SallaPublisher {
public function __construct(
private SallaClient $client,
private SallaMapper $mapper
) {}
public function publish(object $target, array $product): array {
$payload = $this->mapper->toSalla($product);
return $this->client->forTarget($target)->post('/products', $payload);
}
public function hide(object $target, string $externalId): bool {
$r = $this->client->forTarget($target)->put("/products/{$externalId}", [
'status' => 'hidden',
]);
return $r['success'] ?? false;
}
public function show(object $target, string $externalId): bool {
$r = $this->client->forTarget($target)->put("/products/{$externalId}", [
'status' => 'sale',
]);
return $r['success'] ?? false;
}
}
EOF
bash
# Integrations/Salla/SallaClient.php
cat > src/Integrations/Salla/SallaClient.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Integrations\Salla;
use KOSMOS\Studio\Support\Encrypter;
final class SallaClient {
private const BASE_URL = 'https://api.salla.dev/admin/v2';
private ?object $target = null;
public function forTarget(object $target): self {
$clone = clone $this;
$clone->target = $target;
return $clone;
}
public function get(string $path, array $q = []): array {
return $this->request('GET', $path, $q);
}
public function post(string $path, array $body = []): array {
return $this->request('POST', $path, [], $body);
}
public function put(string $path, array $body = []): array {
return $this->request('PUT', $path, [], $body);
}
private function request(string $method, string $path, array $q = [], array $b = []): array {
if (!$this->target) throw new \RuntimeException('Target not set');
$creds = json_decode(Encrypter::decrypt($this->target->credentials), true);
$token = $creds['access_token'] ?? '';
$url = self::BASE_URL . $path;
if ($q) $url .= '?' . http_build_query($q);
$response = wp_remote_request($url, [
'method' => $method,
'timeout' => 30,
'headers' => [
'Authorization' => "Bearer {$token}",
'Content-Type' => 'application/json',
'Accept' => 'application/json',
],
'body' => $b ? wp_json_encode($b) : null,
]);
if (is_wp_error($response)) {
return ['success' => false, 'message' => $response->get_error_message()];
}
return json_decode(wp_remote_retrieve_body($response), true) ?: ['success' => false];
}
}
EOF
๐ ุงูุฏูููุฉ 55-60 | REST Router + Test
bash
# Http/Router.php
cat > src/Http/Router.php << 'EOF'
<?php
declare(strict_types=1);
namespace KOSMOS\Studio\Http;
final class Router {
public function register(): void {
register_rest_route('kosmos/v1', '/health', [
'methods' => 'GET',
'callback' => fn() => [
'status' => 'alive',
'version' => KOSMOS_VERSION,
'time' => current_time('c'),
],
'permission_callback' => '__return_true',
]);
register_rest_route('kosmos/v1', '/sources/(?P<id>\d+)/sync', [
'methods' => 'POST',
'callback' => [new \KOSMOS\Studio\Http\Controllers\SourceController(), 'sync'],
'permission_callback' => fn() => current_user_can('kosmos_sync_sources'),
]);
register_rest_route('kosmos/v1', '/ai/tutor', [
'methods' => 'POST',
'callback' => [new \KOSMOS\Studio\Http\Controllers\AiController(), 'tutor'],
'permission_callback' => fn() => current_user_can('kosmos_use_ai'),
]);
}
}
EOF
bash
# ุงุฎุชุจุฑ ุงูุขู echo "โ KOSMOS Studio โ Skeleton Ready" docker compose up -d sleep 30 curl -s http://localhost:8080/wp-json/kosmos/v1/health | jq
ุงููุชูุฌุฉ ุงูู ุชููุนุฉ:
json
{
"status": "alive",
"version": "1.0.0",
"time": "2025-01-15T10:30:00+00:00"
}
๐ ุงูุฌุฒุก 13: ุฎูุงุตุฉ ุงูู ูุงุตูุฉ
ู ุง ุชู ุชูุซููู ูุงู ูุงู:
| ุงูู ูููู | ุงูุญุงูุฉ |
|---|---|
| โ ูุงุนุฏุฉ ุงูุจูุงูุงุช | 12 ุฌุฏูู + ุนูุงูุงุช + ููุงุฑุณ |
| โ ูููู Plugin | ~80 ู ูู + namespaces |
| โ Domain Classes | ููุฏ ูุงู ู ูู: Tenant, Subscription, Entitlement, Product, Source, Target |
| โ REST API | 45+ endpoint ู ูุซูู |
| โ ูุธุงู ุงูุจุงูุงุช | 4 ุจุงูุงุช + Entitlements |
| โ WooCommerce Hooks | 15+ hook ู ุฑุชุจุท |
| โ AI Gateway | Provider-based + Metering |
| โ Roles & Capabilities | 5 roles + 20 capability |
| โ Dashboards | Tenant + Admin ูุงู ููู |
| โ Job Queue | Atomic claim + Retry + Backoff |
| โ Salla Integration | Client + Publisher + Hide/Show |
| โ Product Lock System | ุงูููู ุงูุชููุงุฆู ุนูุฏ ุงูุชูุงุก ุงูุงุดุชุฑุงู |
| โ ุฎุทุฉ 60 ุฏูููุฉ | 12 ู ุฑุญูุฉ ู ูููุง ุจู ูู |
๐ฏ ุงูุฎุทูุฉ ุงูุชุงููุฉ โ ุงุฎุชุฑ
| # | ุงูุฎูุงุฑ | ุงูู ุญุชูู |
|---|---|---|
| A | ๐ Source Adapters ูุงู ูุฉ | Shopify + WooCommerce + AliExpress + CSV + Custom API |
| B | ๐ณ Payment Gateways ูุงู ูุฉ | Stripe + Moyasar + Tap + PayPal + Webhooks |
| C | ๐จ Frontend ูุงู ู | React/Next.js Dashboard + Pricing + Tutor UI |
| D | ๐งช Test Suite ูุงู ูุฉ | PHPUnit + Integration + E2E + Load (k6) |
| E | ๐ CI/CD + Deployment | GitHub Actions + K8s + Monitoring |
| F | ๐ ูู ู ุง ุณุจู | ุงูู ูุงุตูุฉ ุชูุตุจุญ ู ูุตุฉ ูุงู ูุฉ |
ูู ุงูุญุฑู ูุณุฃูู ู. ๐ฅ