Files
orion/app/modules/tenancy/static/admin/js/merchant-detail.js
Samir Boulahtit 0b37274140 fix(subscriptions): fix subscription UI and API after store→merchant migration
Store detail page now shows all platform subscriptions instead of always
"No Subscription Found". Subscriptions listing page renamed from Store
to Merchant throughout (template, JS, menu, i18n) with Platform column
added. Tiers API supports platform_id filtering.

Merchant detail page no longer hardcodes 'oms' platform — loads all
platforms, shows subscription cards per platform with labels, and the
Create Subscription modal includes a platform selector with
platform-filtered tiers. Create button always accessible in Quick Actions.

Edit modal on /admin/subscriptions loads tiers from API filtered by
platform instead of hardcoded options, sends tier_code (not tier) to
match PATCH schema, and shows platform context.

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-02-10 19:17:51 +01:00

290 lines
12 KiB
JavaScript

// noqa: js-006 - async init pattern is safe, loadData has try/catch
// static/admin/js/merchant-detail.js
// Create custom logger for merchant detail
const merchantDetailLog = window.LogConfig.createLogger('MERCHANT-DETAIL');
function adminMerchantDetail() {
return {
// Inherit base layout functionality from init-alpine.js
...data(),
// Merchant detail page specific state
currentPage: 'merchant-detail',
merchant: null,
loading: false,
error: null,
merchantId: null,
// Subscription state
platforms: [],
subscriptions: [],
tiers: [],
tiersForPlatformId: null,
showCreateSubscriptionModal: false,
createForm: { platform_id: null, tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false },
creatingSubscription: false,
// Initialize
async init() {
// Load i18n translations
await I18n.loadModule('tenancy');
merchantDetailLog.info('=== MERCHANT DETAIL PAGE INITIALIZING ===');
// Prevent multiple initializations
if (window._merchantDetailInitialized) {
merchantDetailLog.warn('Merchant detail page already initialized, skipping...');
return;
}
window._merchantDetailInitialized = true;
// Get merchant ID from URL
const path = window.location.pathname;
const match = path.match(/\/admin\/merchants\/(\d+)$/);
if (match) {
this.merchantId = match[1];
merchantDetailLog.info('Viewing merchant:', this.merchantId);
await this.loadMerchant();
await this.loadPlatforms();
await this.loadSubscriptions();
} else {
merchantDetailLog.error('No merchant ID in URL');
this.error = 'Invalid merchant URL';
Utils.showToast(I18n.t('tenancy.messages.invalid_merchant_url'), 'error');
}
merchantDetailLog.info('=== MERCHANT DETAIL PAGE INITIALIZATION COMPLETE ===');
},
// Load merchant data
async loadMerchant() {
merchantDetailLog.info('Loading merchant details...');
this.loading = true;
this.error = null;
try {
const url = `/admin/merchants/${this.merchantId}`;
window.LogConfig.logApiCall('GET', url, null, 'request');
const startTime = performance.now();
const response = await apiClient.get(url);
const duration = performance.now() - startTime;
window.LogConfig.logApiCall('GET', url, response, 'response');
window.LogConfig.logPerformance('Load Merchant Details', duration);
this.merchant = response;
merchantDetailLog.info(`Merchant loaded in ${duration}ms`, {
id: this.merchant.id,
name: this.merchant.name,
is_verified: this.merchant.is_verified,
is_active: this.merchant.is_active,
store_count: this.merchant.store_count
});
merchantDetailLog.debug('Full merchant data:', this.merchant);
} catch (error) {
window.LogConfig.logError(error, 'Load Merchant Details');
this.error = error.message || 'Failed to load merchant details';
Utils.showToast(I18n.t('tenancy.messages.failed_to_load_merchant_details'), 'error');
} finally {
this.loading = false;
}
},
// Load all available platforms
async loadPlatforms() {
try {
const response = await apiClient.get('/admin/platforms');
this.platforms = (response.platforms || []).map(p => ({ id: p.id, name: p.name, code: p.code }));
merchantDetailLog.info('Platforms loaded:', this.platforms.length);
} catch (error) {
merchantDetailLog.warn('Failed to load platforms:', error.message);
}
},
// Load subscriptions for all platforms
async loadSubscriptions() {
if (!this.merchantId || this.platforms.length === 0) return;
merchantDetailLog.info('Loading subscriptions for merchant:', this.merchantId);
this.subscriptions = [];
for (const platform of this.platforms) {
try {
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platform.id}`;
const response = await apiClient.get(url);
const sub = response.subscription || response;
this.subscriptions.push({
subscription: sub,
tier: response.tier || null,
features: response.features || [],
platform_id: platform.id,
platform_name: platform.name,
});
} catch (error) {
if (error.status !== 404) {
merchantDetailLog.warn(`Failed to load subscription for platform ${platform.name}:`, error.message);
}
}
}
merchantDetailLog.info('Subscriptions loaded:', {
count: this.subscriptions.length,
platforms: this.subscriptions.map(e => e.platform_name)
});
},
// Load available subscription tiers for a platform
async loadTiers(platformId) {
if (this.tiers.length > 0 && this.tiersForPlatformId === platformId) return;
try {
const url = platformId
? `/admin/subscriptions/tiers?platform_id=${platformId}`
: '/admin/subscriptions/tiers';
const response = await apiClient.get(url);
this.tiers = response.tiers || [];
this.tiersForPlatformId = platformId;
merchantDetailLog.info('Loaded tiers:', this.tiers.length);
} catch (error) {
merchantDetailLog.warn('Failed to load tiers:', error.message);
}
},
// Open create subscription modal
async openCreateSubscriptionModal() {
const firstPlatformId = this.platforms.length > 0 ? this.platforms[0].id : null;
this.createForm = { platform_id: firstPlatformId, tier_code: 'essential', status: 'trial', trial_days: 14, is_annual: false };
await this.loadTiers(firstPlatformId);
this.showCreateSubscriptionModal = true;
},
// Reload tiers when platform changes in create modal
async onCreatePlatformChange() {
this.tiers = [];
this.tiersForPlatformId = null;
await this.loadTiers(this.createForm.platform_id);
},
// Create subscription for this merchant
async createSubscription() {
if (!this.merchantId || !this.createForm.platform_id) return;
this.creatingSubscription = true;
const platformId = this.createForm.platform_id;
merchantDetailLog.info('Creating subscription for merchant:', this.merchantId, 'platform:', platformId);
try {
const url = `/admin/subscriptions/merchants/${this.merchantId}/platforms/${platformId}`;
const payload = {
merchant_id: parseInt(this.merchantId),
platform_id: platformId,
tier_code: this.createForm.tier_code,
status: this.createForm.status,
trial_days: this.createForm.status === 'trial' ? parseInt(this.createForm.trial_days) : 0,
is_annual: this.createForm.is_annual
};
window.LogConfig.logApiCall('POST', url, payload, 'request');
const response = await apiClient.post(url, payload);
window.LogConfig.logApiCall('POST', url, response, 'response');
this.showCreateSubscriptionModal = false;
Utils.showToast('Subscription created successfully', 'success');
merchantDetailLog.info('Subscription created');
await this.loadSubscriptions();
} catch (error) {
window.LogConfig.logError(error, 'Create Subscription');
Utils.showToast(error.message || 'Failed to create subscription', 'error');
} finally {
this.creatingSubscription = false;
}
},
// Get usage bar color based on percentage
getUsageBarColor(current, limit) {
if (!limit || limit === 0) return 'bg-blue-500';
const percent = (current / limit) * 100;
if (percent >= 90) return 'bg-red-500';
if (percent >= 75) return 'bg-yellow-500';
return 'bg-green-500';
},
// Format tier price for display
formatTierPrice(tier) {
if (!tier.price_monthly_cents) return 'Custom';
return `${(tier.price_monthly_cents / 100).toFixed(2)}/mo`;
},
// Format date (matches dashboard pattern)
formatDate(dateString) {
if (!dateString) {
merchantDetailLog.debug('formatDate called with empty dateString');
return '-';
}
const formatted = Utils.formatDate(dateString);
merchantDetailLog.debug(`Date formatted: ${dateString} -> ${formatted}`);
return formatted;
},
// Delete merchant
async deleteMerchant() {
merchantDetailLog.info('Delete merchant requested:', this.merchantId);
if (this.merchant?.store_count > 0) {
Utils.showToast(`Cannot delete merchant with ${this.merchant.store_count} store(s). Delete stores first.`, 'error');
return;
}
if (!confirm(`Are you sure you want to delete merchant "${this.merchant.name}"?\n\nThis action cannot be undone.`)) {
merchantDetailLog.info('Delete cancelled by user');
return;
}
// Second confirmation for safety
if (!confirm(`FINAL CONFIRMATION\n\nAre you absolutely sure you want to delete "${this.merchant.name}"?`)) {
merchantDetailLog.info('Delete cancelled by user (second confirmation)');
return;
}
try {
const url = `/admin/merchants/${this.merchantId}?confirm=true`;
window.LogConfig.logApiCall('DELETE', url, null, 'request');
merchantDetailLog.info('Deleting merchant:', this.merchantId);
await apiClient.delete(url);
window.LogConfig.logApiCall('DELETE', url, null, 'response');
Utils.showToast(I18n.t('tenancy.messages.merchant_deleted_successfully'), 'success');
merchantDetailLog.info('Merchant deleted successfully');
// Redirect to merchants list
setTimeout(() => window.location.href = '/admin/merchants', 1500);
} catch (error) {
window.LogConfig.logError(error, 'Delete Merchant');
Utils.showToast(error.message || 'Failed to delete merchant', 'error');
}
},
// Refresh merchant data
async refresh() {
merchantDetailLog.info('=== MERCHANT REFRESH TRIGGERED ===');
await this.loadMerchant();
await this.loadSubscriptions();
Utils.showToast(I18n.t('tenancy.messages.merchant_details_refreshed'), 'success');
merchantDetailLog.info('=== MERCHANT REFRESH COMPLETE ===');
}
};
}
merchantDetailLog.info('Merchant detail module loaded');