EverydayTech Platform - Developer Reference
Complete Source Code Documentation - All Applications
Loading...
Searching...
No Matches
office365.js
Go to the documentation of this file.
1/**
2 * @file office365.js
3 * @module Office365Routes
4 * @description Office 365 / Pax8 integration API routes. Manages Microsoft 365 subscription lifecycle through Pax8 partner API, including configuration management, customer synchronization, product catalog access, and subscription CRUD operations. Multi-tenant aware with full isolation.
5 * @see {@link ../services/pax8Service} for Pax8 API integration
6 * @see {@link ../services/db} for database connection
7 * @see {@link ../middleware/auth} for authentication middleware
8 * @see {@link ../middleware/tenant} for tenant context utilities
9 * @apiDefine Office365Group Office365
10 * @apiGroup Office365
11 * @apiHeader {string} Authorization Bearer token required.
12 * @apiHeader {string} X-Tenant-ID Tenant context header required.
13 * @apiError (Error 401) Unauthorized Missing or invalid token.
14 * @apiError (Error 403) Forbidden Tenant context missing or invalid.
15 * @apiError (Error 500) ServerError Internal server error or Pax8 API error.
16 */
17
18const express = require('express');
19const router = express.Router();
20const pool = require('../services/db');
21const authenticateToken = require('../middleware/auth');
22const { getTenantFilter, setTenantContext } = require('../middleware/tenant');
23const Pax8Service = require('../services/pax8Service');
24
25// Apply authentication and tenant context to all routes
26router.use(authenticateToken, setTenantContext);
27
28// ========================================
29// Configuration Management
30// ========================================
31
32/**
33 * @api {get} /office365/config Get Pax8 configuration
34 * @apiName GetPax8Config
35 * @apiGroup Office365
36 * @apiDescription Retrieve Pax8 API configuration for the current tenant (without exposing secrets). Returns configuration status and metadata.
37 * @apiSuccess {boolean} configured Whether Pax8 is configured for this tenant.
38 * @apiSuccess {object} [config] Configuration object (if configured).
39 * @apiSuccess {string} config.config_id Configuration ID.
40 * @apiSuccess {string} config.tenant_id Tenant ID.
41 * @apiSuccess {string} config.client_id Pax8 client ID.
42 * @apiSuccess {string} config.pax8_company_id Pax8 company ID.
43 * @apiSuccess {boolean} config.is_active Whether configuration is active.
44 * @apiSuccess {Date} config.last_sync_at Last synchronization timestamp.
45 * @apiSuccess {string} config.last_sync_status Last sync status (success/failed).
46 * @apiExample {curl} Example usage:
47 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/config
48 */
49router.get('/config', async (req, res) => {
50 try {
51 const result = await pool.query(
52 `SELECT config_id, tenant_id, client_id, pax8_company_id, is_active,
53 last_sync_at, last_sync_status, sync_error, created_at, updated_at
54 FROM pax8_config
55 WHERE tenant_id = $1`,
56 [req.tenant.id]
57 );
58
59 if (result.rows.length === 0) {
60 return res.json({ configured: false });
61 }
62
63 res.json({ configured: true, config: result.rows[0] });
64 } catch (err) {
65 console.error('Error fetching Pax8 config:', err);
66 res.status(500).json({ error: 'Server error', details: err.message });
67 }
68});
69
70/**
71 * @api {put} /office365/config Update Pax8 configuration
72 * @apiName UpdatePax8Config
73 * @apiGroup Office365
74 * @apiDescription Update or create Pax8 API configuration for the current tenant. Stores client credentials for Pax8 partner API integration.
75 * @apiParam {string} client_id Pax8 OAuth client ID.
76 * @apiParam {string} client_secret Pax8 OAuth client secret (will be encrypted).
77 * @apiParam {string} [pax8_company_id] Pax8 company ID.
78 * @apiSuccess {string} message Success confirmation.
79 * @apiSuccess {object} config Saved configuration object.
80 * @apiError (Error 400) BadRequest client_id and client_secret are required.
81 * @apiExample {curl} Example usage:
82 * curl -X PUT -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" -d '{"client_id":"abc","client_secret":"secret","pax8_company_id":"123"}' https://api.example.com/office365/config
83 */
84router.put('/config', async (req, res) => {
85 const { client_id, client_secret, pax8_company_id } = req.body;
86
87 if (!client_id || !client_secret) {
88 return res.status(400).json({ error: 'client_id and client_secret are required' });
89 }
90
91 try {
92 // TODO: Encrypt client_secret before storing
93 const encrypted_secret = client_secret; // Implement proper encryption
94
95 // Upsert config
96 const result = await pool.query(
97 `INSERT INTO pax8_config (tenant_id, client_id, client_secret_encrypted, pax8_company_id, updated_at)
98 VALUES ($1, $2, $3, $4, NOW())
99 ON CONFLICT (tenant_id)
100 DO UPDATE SET
101 client_id = $2,
102 client_secret_encrypted = $3,
103 pax8_company_id = $4,
104 updated_at = NOW()
105 RETURNING config_id, tenant_id, client_id, pax8_company_id, is_active, created_at, updated_at`,
106 [req.tenant.id, client_id, encrypted_secret, pax8_company_id]
107 );
108
109 res.json({ message: 'Configuration saved successfully', config: result.rows[0] });
110 } catch (err) {
111 console.error('Error saving Pax8 config:', err);
112 res.status(500).json({ error: 'Server error', details: err.message });
113 }
114});
115
116/**
117 * @api {post} /office365/config/test Test Pax8 API connection
118 * @apiName TestPax8Connection
119 * @apiGroup Office365
120 * @apiDescription Test the Pax8 API connection with current tenant's configuration. Updates sync status in database.
121 * @apiSuccess {boolean} success Connection test result.
122 * @apiSuccess {string} message Status message.
123 * @apiError (Error 400) BadRequest Connection test failed.
124 * @apiError (Error 500) ServerError API error or configuration missing.
125 * @apiExample {curl} Example usage:
126 * curl -X POST -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/config/test
127 */
128router.post('/config/test', async (req, res) => {
129 try {
130 const pax8 = new Pax8Service(req.tenant.id);
131 const success = await pax8.testConnection();
132
133 if (success) {
134 await pax8.updateSyncStatus('success');
135 res.json({ success: true, message: 'Connection successful' });
136 } else {
137 await pax8.updateSyncStatus('failed', 'Connection test failed');
138 res.status(400).json({ success: false, error: 'Connection test failed' });
139 }
140 } catch (err) {
141 console.error('Error testing Pax8 connection:', err);
142 await new Pax8Service(req.tenant.id).updateSyncStatus('failed', err.message);
143 res.status(500).json({ success: false, error: err.message });
144 }
145});
146
147// ========================================
148// Customer Sync
149// ========================================
150
151/**
152 * @api {post} /office365/customers/:customerId/sync Sync customer to Pax8
153 * @apiName SyncCustomerToPax8
154 * @apiGroup Office365
155 * @apiDescription Sync a specific customer to Pax8 (create new company or update existing). Creates mapping record in database for future synchronization. If customer already synced, updates from Pax8.
156 * @apiParam {number} customerId Customer ID (URL parameter).
157 * @apiSuccess {string} message Success confirmation.
158 * @apiSuccess {object} mapping Pax8 customer mapping object.
159 * @apiSuccess {string} mapping.pax8_company_id Pax8 company ID.
160 * @apiSuccess {string} mapping.pax8_company_name Pax8 company name.
161 * @apiSuccess {string} mapping.sync_status Sync status (synced/failed).
162 * @apiError (Error 404) NotFound Customer not found.
163 * @apiError (Error 500) ServerError Pax8 API error or sync failed.
164 * @apiExample {curl} Example usage:
165 * curl -X POST -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/customers/123/sync
166 */
167router.post('/customers/:customerId/sync', async (req, res) => {
168 const customerId = parseInt(req.params.customerId);
169
170 try {
171 const pax8 = new Pax8Service(req.tenant.id);
172
173 // Get local customer
174 const customerResult = await pool.query(
175 'SELECT * FROM customers WHERE customer_id = $1 AND tenant_id = $2',
176 [customerId, req.tenant.id]
177 );
178
179 if (customerResult.rows.length === 0) {
180 return res.status(404).json({ error: 'Customer not found' });
181 }
182
183 const customer = customerResult.rows[0];
184
185 // Check if already synced
186 const mappingResult = await pool.query(
187 'SELECT * FROM pax8_customers WHERE customer_id = $1 AND tenant_id = $2',
188 [customerId, req.tenant.id]
189 );
190
191 if (mappingResult.rows.length > 0) {
192 // Already synced
193 const mapping = mappingResult.rows[0];
194 const pax8Customer = await pax8.getCompany(mapping.pax8_company_id);
195
196 await pool.query(
197 'UPDATE pax8_customers SET pax8_company_name = $1, last_synced_at = NOW(), sync_status = $2 WHERE id = $3',
198 [pax8Customer.name, 'synced', mapping.id]
199 );
200
201 return res.json({
202 message: 'Customer already synced, updated from Pax8',
203 mapping: { ...mapping, pax8_company_name: pax8Customer.name }
204 });
205 }
206
207 // Create in Pax8
208 const pax8CustomerData = {
209 name: customer.name,
210 address: {
211 street: customer.address || '',
212 city: customer.city || '',
213 state: customer.state || '',
214 postalCode: customer.postal_code || '',
215 country: customer.country || 'AU'
216 },
217 contact: {
218 name: customer.contact_name || customer.name,
219 email: customer.email || '',
220 phone: customer.phone || ''
221 }
222 };
223
224 const pax8Customer = await pax8.createCompany(pax8CustomerData);
225
226 // Save mapping
227 const insertResult = await pool.query(
228 `INSERT INTO pax8_customers (tenant_id, customer_id, pax8_company_id, pax8_company_name, last_synced_at, sync_status)
229 VALUES ($1, $2, $3, $4, NOW(), 'synced')
230 RETURNING *`,
231 [req.tenant.id, customerId, pax8Customer.id, pax8Customer.name]
232 );
233
234 res.json({
235 message: 'Customer synced successfully',
236 mapping: insertResult.rows[0]
237 });
238 } catch (err) {
239 console.error('Error syncing customer:', err);
240
241 // Update sync status to failed
242 await pool.query(
243 `UPDATE pax8_customers
244 SET sync_status = 'failed', sync_error = $1
245 WHERE customer_id = $2 AND tenant_id = $3`,
246 [err.message, customerId, req.tenant.id]
247 );
248
249 res.status(500).json({ error: 'Failed to sync customer', details: err.message });
250 }
251});
252
253/**
254 * @api {get} /office365/customers/:customerId/sync-status Get customer sync status
255 * @apiName GetCustomerSyncStatus
256 * @apiGroup Office365
257 * @apiDescription Check if a customer is synced to Pax8 and retrieve sync status details.
258 * @apiParam {number} customerId Customer ID (URL parameter).
259 * @apiSuccess {boolean} synced Whether customer is synced.
260 * @apiSuccess {object} [mapping] Sync mapping object (if synced).
261 * @apiExample {curl} Example usage:
262 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/customers/123/sync-status
263 */
264router.get('/customers/:customerId/sync-status', async (req, res) => {
265 try {
266 const result = await pool.query(
267 'SELECT * FROM pax8_customers WHERE customer_id = $1 AND tenant_id = $2',
268 [req.params.customerId, req.tenant.id]
269 );
270
271 if (result.rows.length === 0) {
272 return res.json({ synced: false });
273 }
274
275 res.json({ synced: true, mapping: result.rows[0] });
276 } catch (err) {
277 console.error('Error checking sync status:', err);
278 res.status(500).json({ error: 'Server error', details: err.message });
279 }
280});
281
282// ========================================
283// Products & SKUs
284// ========================================
285
286/**
287 * @api {get} /office365/products List Microsoft 365 products from Pax8
288 * @apiName ListPax8Products
289 * @apiGroup Office365
290 * @apiDescription Retrieve available Microsoft 365 products from Pax8 catalog. Supports filtering by vendor and other query parameters.
291 * @apiParam {string} [vendor=Microsoft] Filter by vendor name.
292 * @apiSuccess {object[]} products Array of product objects from Pax8.
293 * @apiExample {curl} Example usage:
294 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/products?vendor=Microsoft
295 */
296router.get('/products', async (req, res) => {
297 try {
298 const pax8 = new Pax8Service(req.tenant.id);
299 const filters = {
300 vendor: req.query.vendor || 'Microsoft',
301 ...req.query
302 };
303
304 const products = await pax8.listProducts(filters);
305
306 res.json({ products: products.content || products });
307 } catch (err) {
308 console.error('Error fetching products:', err);
309 res.status(500).json({ error: 'Failed to fetch products', details: err.message });
310 }
311});
312
313/**
314 * @api {get} /office365/products/:productId Get product details
315 * @apiName GetPax8Product
316 * @apiGroup Office365
317 * @apiDescription Retrieve detailed information about a specific product from Pax8 catalog by product ID.
318 * @apiParam {string} productId Pax8 product ID (URL parameter).
319 * @apiSuccess {object} product Product details object from Pax8.
320 * @apiExample {curl} Example usage:
321 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/products/prod-123
322 */
323router.get('/products/:productId', async (req, res) => {
324 try {
325 const pax8 = new Pax8Service(req.tenant.id);
326 const product = await pax8.getProduct(req.params.productId);
327
328 res.json({ product });
329 } catch (err) {
330 console.error('Error fetching product:', err);
331 res.status(500).json({ error: 'Failed to fetch product', details: err.message });
332 }
333});
334
335/**
336 * @api {post} /office365/products/sync Sync products from Pax8 to local database
337 * @apiName SyncPax8Products
338 * @apiGroup Office365
339 * @apiDescription Fetch Microsoft 365 products from Pax8 and sync to local database for caching and offline access. Updates existing products or inserts new ones.
340 * @apiParam {string} [vendor=Microsoft] Filter by vendor name (query param).
341 * @apiParam {boolean} [force=false] Force re-sync even if recently synced (query param).
342 * @apiSuccess {string} message Success confirmation.
343 * @apiSuccess {number} synced Count of newly synced products.
344 * @apiSuccess {number} updated Count of updated products.
345 * @apiSuccess {number} skipped Count of skipped products.
346 * @apiExample {curl} Example usage:
347 * curl -X POST -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/products/sync?vendor=Microsoft&force=true
348 */
349router.post('/products/sync', async (req, res) => {
350 try {
351 const pax8 = new Pax8Service(req.tenant.id);
352 const vendor = req.query.vendor || 'Microsoft';
353 const force = req.query.force === 'true';
354
355 console.log(`[Office365] Starting product sync for vendor: ${vendor}`);
356
357 // Fetch products from Pax8
358 const productsResponse = await pax8.listProducts({ vendor });
359 const products = productsResponse.content || productsResponse || [];
360
361 console.log(`[Office365] Found ${products.length} products from Pax8`);
362
363 let syncedCount = 0;
364 let updatedCount = 0;
365 let skippedCount = 0;
366
367 for (const pax8Product of products) {
368 try {
369 // Check if product already exists
370 const existingProduct = await pool.query(
371 'SELECT product_id, last_synced_at FROM products WHERE pax8_product_id = $1',
372 [pax8Product.id]
373 );
374
375 // Skip if recently synced (within last hour) unless force=true
376 if (existingProduct.rows.length > 0 && !force) {
377 const lastSynced = existingProduct.rows[0].last_synced_at;
378 if (lastSynced && (Date.now() - new Date(lastSynced).getTime()) < 3600000) {
379 skippedCount++;
380 continue;
381 }
382 }
383
384 const productData = {
385 name: pax8Product.name || pax8Product.productName,
386 description: pax8Product.description || pax8Product.productDescription || '',
387 supplier: vendor,
388 is_service: true, // Office 365 is a service
389 divisible: true, // Can be sold per user
390 is_stock: false, // Digital product
391 price_retail: pax8Product.pricing?.retail || 0,
392 price_ex_tax: pax8Product.pricing?.cost || 0,
393 pax8_product_id: pax8Product.id,
394 pax8_vendor: vendor,
395 pax8_category: pax8Product.category || 'Software',
396 pax8_subcategory: pax8Product.subcategory || '',
397 pax8_sku: pax8Product.sku || pax8Product.vendorSku || '',
398 billing_term: pax8Product.billingTerm || 'monthly',
399 is_auto_renew: pax8Product.autoRenew !== false,
400 pax8_metadata: JSON.stringify(pax8Product),
401 is_active: pax8Product.status === 'Active' || pax8Product.available !== false,
402 last_synced_at: new Date()
403 };
404
405 if (existingProduct.rows.length > 0) {
406 // Update existing product
407 await pool.query(
408 `UPDATE products SET
409 name = $1, description = $2, supplier = $3,
410 price_retail = $4, price_ex_tax = $5,
411 pax8_vendor = $6, pax8_category = $7, pax8_subcategory = $8,
412 pax8_sku = $9, billing_term = $10, is_auto_renew = $11,
413 pax8_metadata = $12, is_active = $13, last_synced_at = $14
414 WHERE pax8_product_id = $15`,
415 [
416 productData.name, productData.description, productData.supplier,
417 productData.price_retail, productData.price_ex_tax,
418 productData.pax8_vendor, productData.pax8_category, productData.pax8_subcategory,
419 productData.pax8_sku, productData.billing_term, productData.is_auto_renew,
420 productData.pax8_metadata, productData.is_active, productData.last_synced_at,
421 pax8Product.id
422 ]
423 );
424 updatedCount++;
425 } else {
426 // Insert new product
427 await pool.query(
428 `INSERT INTO products (
429 name, description, supplier, is_service, divisible, is_stock,
430 price_retail, price_ex_tax, pax8_product_id, pax8_vendor,
431 pax8_category, pax8_subcategory, pax8_sku, billing_term,
432 is_auto_renew, pax8_metadata, is_active, last_synced_at
433 ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17, $18)`,
434 [
435 productData.name, productData.description, productData.supplier,
436 productData.is_service, productData.divisible, productData.is_stock,
437 productData.price_retail, productData.price_ex_tax, productData.pax8_product_id,
438 productData.pax8_vendor, productData.pax8_category, productData.pax8_subcategory,
439 productData.pax8_sku, productData.billing_term, productData.is_auto_renew,
440 productData.pax8_metadata, productData.is_active, productData.last_synced_at
441 ]
442 );
443 syncedCount++;
444 }
445 } catch (productErr) {
446 console.error(`[Office365] Error syncing product ${pax8Product.id}:`, productErr);
447 // Continue with next product
448 }
449 }
450
451 console.log(`[Office365] Sync complete: ${syncedCount} new, ${updatedCount} updated, ${skippedCount} skipped`);
452
453 res.json({
454 success: true,
455 message: 'Products synced successfully',
456 stats: {
457 total: products.length,
458 synced: syncedCount,
459 updated: updatedCount,
460 skipped: skippedCount
461 }
462 });
463 } catch (err) {
464 console.error('Error syncing products:', err);
465 res.status(500).json({ error: 'Failed to sync products', details: err.message });
466 }
467});
468
469// ========================================
470// Subscriptions
471// ========================================
472
473/**
474 * @api {get} /office365/subscriptions List Office 365 subscriptions
475 * @apiName ListOffice365Subscriptions
476 * @apiGroup Office365
477 * @apiDescription Retrieve all Office 365 subscriptions for the current tenant with customer and contract details.
478 * @apiSuccess {object[]} subscriptions Array of subscription objects.
479 * @apiSuccess {number} subscriptions.subscription_id Subscription ID.
480 * @apiSuccess {string} subscriptions.pax8_subscription_id Pax8 subscription ID.
481 * @apiSuccess {number} subscriptions.customer_id Customer ID.
482 * @apiSuccess {string} subscriptions.customer_name Customer name.
483 * @apiSuccess {string} subscriptions.sku_name Product/SKU name.
484 * @apiSuccess {number} subscriptions.quantity License quantity.
485 * @apiSuccess {string} subscriptions.status Subscription status.
486 * @apiExample {curl} Example usage:
487 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/subscriptions
488 */
489router.get('/subscriptions', async (req, res) => {
490 try {
491 const { clause, params } = getTenantFilter(req, 's');
492 let query = `
493 SELECT s.*, c.name as customer_name, co.contract_name
494 FROM office365_subscriptions s
495 LEFT JOIN customers c ON s.customer_id = c.customer_id
496 LEFT JOIN contracts co ON s.contract_id = co.contract_id
497 `;
498
499 if (clause) {
500 query += ` WHERE ${clause}`;
501 }
502
503 query += ' ORDER BY s.created_at DESC';
504
505 const result = await pool.query(query, params);
506 res.json({ subscriptions: result.rows });
507 } catch (err) {
508 console.error('Error fetching subscriptions:', err);
509 res.status(500).json({ error: 'Server error', details: err.message });
510 }
511});
512
513/**
514 * @api {get} /office365/subscriptions/:id Get subscription details
515 * @apiName GetOffice365Subscription
516 * @apiGroup Office365
517 * @apiDescription Retrieve detailed subscription information including license assignments.
518 * @apiParam {number} id Subscription ID (URL parameter).
519 * @apiSuccess {object} subscription Subscription details.
520 * @apiSuccess {object[]} assignments License assignments for this subscription.
521 * @apiError (Error 404) NotFound Subscription not found.
522 * @apiExample {curl} Example usage:
523 * curl -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/subscriptions/123
524 */
525router.get('/subscriptions/:id', async (req, res) => {
526 try {
527 const subResult = await pool.query(
528 `SELECT s.*, c.name as customer_name, co.contract_name
529 FROM office365_subscriptions s
530 LEFT JOIN customers c ON s.customer_id = c.customer_id
531 LEFT JOIN contracts co ON s.contract_id = co.contract_id
532 WHERE s.subscription_id = $1 AND s.tenant_id = $2`,
533 [req.params.id, req.tenant.id]
534 );
535
536 if (subResult.rows.length === 0) {
537 return res.status(404).json({ error: 'Subscription not found' });
538 }
539
540 const subscription = subResult.rows[0];
541
542 // Get license assignments
543 const assignmentsResult = await pool.query(
544 'SELECT * FROM office365_license_assignments WHERE subscription_id = $1 ORDER BY assigned_at DESC',
545 [req.params.id]
546 );
547
548 res.json({
549 subscription,
550 assignments: assignmentsResult.rows
551 });
552 } catch (err) {
553 console.error('Error fetching subscription:', err);
554 res.status(500).json({ error: 'Server error', details: err.message });
555 }
556});
557
558/**
559 * @api {post} /office365/subscriptions Create new subscription
560 * @apiName CreateOffice365Subscription
561 * @apiGroup Office365
562 * @apiDescription Purchase Microsoft 365 licenses via Pax8 and create subscription record. Customer must be synced to Pax8 first.
563 * @apiParam {number} customer_id Customer ID.
564 * @apiParam {string} product_id Pax8 product ID.
565 * @apiParam {number} quantity License quantity.
566 * @apiParam {string} [sku] Product SKU.
567 * @apiParam {string} [billing_cycle=monthly] Billing cycle (monthly/yearly).
568 * @apiParam {number} [contract_id] Associated contract ID.
569 * @apiSuccess {string} message Success confirmation.
570 * @apiSuccess {object} subscription Created subscription object.
571 * @apiError (Error 400) BadRequest Required fields missing or customer not synced.
572 * @apiError (Error 500) ServerError Pax8 API error or database error.
573 * @apiExample {curl} Example usage:
574 * curl -X POST -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" -d '{"customer_id":123,"product_id":"prod-abc","quantity":10}' https://api.example.com/office365/subscriptions
575 */
576router.post('/subscriptions', async (req, res) => {
577 const { customer_id, product_id, sku, quantity, billing_cycle, contract_id } = req.body;
578
579 if (!customer_id || !product_id || !quantity) {
580 return res.status(400).json({ error: 'customer_id, product_id, and quantity are required' });
581 }
582
583 try {
584 // Get Pax8 customer mapping
585 const mappingResult = await pool.query(
586 'SELECT * FROM pax8_customers WHERE customer_id = $1 AND tenant_id = $2',
587 [customer_id, req.tenant.id]
588 );
589
590 if (mappingResult.rows.length === 0) {
591 return res.status(400).json({
592 error: 'Customer not synced with Pax8',
593 message: 'Please sync the customer with Pax8 before creating subscriptions'
594 });
595 }
596
597 const pax8Customer = mappingResult.rows[0];
598
599 // Create subscription in Pax8
600 const pax8 = new Pax8Service(req.tenant.id);
601 const pax8Subscription = await pax8.createSubscription({
602 companyId: pax8Customer.pax8_company_id,
603 productId: product_id,
604 quantity: quantity,
605 billingTerm: billing_cycle || 'monthly'
606 });
607
608 // Save to database
609 const insertResult = await pool.query(
610 `INSERT INTO office365_subscriptions
611 (tenant_id, customer_id, contract_id, pax8_subscription_id, pax8_company_id,
612 product_id, sku, sku_name, quantity, unit_price, total_price, currency,
613 status, billing_cycle, start_date, renewal_date, metadata)
614 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14, $15, $16, $17)
615 RETURNING *`,
616 [
617 req.tenant.id,
618 customer_id,
619 contract_id,
620 pax8Subscription.id,
621 pax8Customer.pax8_company_id,
622 product_id,
623 sku || pax8Subscription.sku,
624 pax8Subscription.productName || pax8Subscription.name,
625 quantity,
626 pax8Subscription.unitPrice || pax8Subscription.price,
627 pax8Subscription.totalPrice || (pax8Subscription.price * quantity),
628 'AUD',
629 'active',
630 billing_cycle || 'monthly',
631 pax8Subscription.startDate || new Date(),
632 pax8Subscription.renewalDate,
633 JSON.stringify(pax8Subscription)
634 ]
635 );
636
637 // Log history
638 await pool.query(
639 `INSERT INTO office365_subscription_history
640 (subscription_id, tenant_id, action, new_quantity, new_status, changed_by, contract_id)
641 VALUES ($1, $2, 'created', $3, 'active', $4, $5)`,
642 [insertResult.rows[0].subscription_id, req.tenant.id, quantity, req.user.id, contract_id]
643 );
644
645 res.json({
646 message: 'Subscription created successfully',
647 subscription: insertResult.rows[0]
648 });
649 } catch (err) {
650 console.error('Error creating subscription:', err);
651 res.status(500).json({ error: 'Failed to create subscription', details: err.message });
652 }
653});
654
655/**
656 * @api {patch} /office365/subscriptions/:id Update subscription quantity
657 * @apiName UpdateOffice365Subscription
658 * @apiGroup Office365
659 * @apiDescription Update subscription license quantity. Changes are synced to Pax8 and logged in subscription history.
660 * @apiParam {number} id Subscription ID (URL parameter).
661 * @apiParam {number} quantity New license quantity.
662 * @apiSuccess {string} message Success confirmation.
663 * @apiSuccess {object} subscription Updated subscription object.
664 * @apiError (Error 400) BadRequest Quantity is required.
665 * @apiError (Error 404) NotFound Subscription not found.
666 * @apiError (Error 500) ServerError Pax8 API error or database error.
667 * @apiExample {curl} Example usage:
668 * curl -X PATCH -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" -d '{"quantity":15}' https://api.example.com/office365/subscriptions/123
669 */
670router.patch('/subscriptions/:id', async (req, res) => {
671 const { quantity } = req.body;
672
673 if (!quantity || quantity < 1) {
674 return res.status(400).json({ error: 'Valid quantity is required' });
675 }
676
677 try {
678 // Get subscription
679 const subResult = await pool.query(
680 'SELECT * FROM office365_subscriptions WHERE subscription_id = $1 AND tenant_id = $2',
681 [req.params.id, req.tenant.id]
682 );
683
684 if (subResult.rows.length === 0) {
685 return res.status(404).json({ error: 'Subscription not found' });
686 }
687
688 const subscription = subResult.rows[0];
689
690 // Update in Pax8
691 const pax8 = new Pax8Service(req.tenant.id);
692 const pax8Subscription = await pax8.updateSubscription(subscription.pax8_subscription_id, {
693 quantity: quantity
694 });
695
696 // Update database
697 const updateResult = await pool.query(
698 `UPDATE office365_subscriptions
699 SET quantity = $1, total_price = $2, updated_at = NOW()
700 WHERE subscription_id = $3
701 RETURNING *`,
702 [quantity, pax8Subscription.totalPrice || (quantity * subscription.unit_price), req.params.id]
703 );
704
705 // Log history
706 await pool.query(
707 `INSERT INTO office365_subscription_history
708 (subscription_id, tenant_id, action, previous_quantity, new_quantity, changed_by)
709 VALUES ($1, $2, 'quantity_changed', $3, $4, $5)`,
710 [req.params.id, req.tenant.id, subscription.quantity, quantity, req.user.id]
711 );
712
713 res.json({
714 message: 'Subscription updated successfully',
715 subscription: updateResult.rows[0]
716 });
717 } catch (err) {
718 console.error('Error updating subscription:', err);
719 res.status(500).json({ error: 'Failed to update subscription', details: err.message });
720 }
721});
722
723/**
724 * @api {delete} /office365/subscriptions/:id Cancel subscription
725 * @apiName CancelOffice365Subscription
726 * @apiGroup Office365
727 * @apiDescription Cancel a Microsoft 365 subscription in Pax8 and update status to 'cancelled'. Changes are logged in subscription history.
728 * @apiParam {number} id Subscription ID (URL parameter).
729 * @apiSuccess {string} message Success confirmation.
730 * @apiError (Error 404) NotFound Subscription not found.
731 * @apiError (Error 500) ServerError Pax8 API error or database error.
732 * @apiExample {curl} Example usage:
733 * curl -X DELETE -H "Authorization: Bearer <token>" -H "X-Tenant-ID: <id>" https://api.example.com/office365/subscriptions/123
734 */
735router.delete('/subscriptions/:id', async (req, res) => {
736 try {
737 // Get subscription
738 const subResult = await pool.query(
739 'SELECT * FROM office365_subscriptions WHERE subscription_id = $1 AND tenant_id = $2',
740 [req.params.id, req.tenant.id]
741 );
742
743 if (subResult.rows.length === 0) {
744 return res.status(404).json({ error: 'Subscription not found' });
745 }
746
747 const subscription = subResult.rows[0];
748
749 // Cancel in Pax8
750 const pax8 = new Pax8Service(req.tenant.id);
751 await pax8.cancelSubscription(subscription.pax8_subscription_id);
752
753 // Update database
754 await pool.query(
755 `UPDATE office365_subscriptions
756 SET status = 'cancelled', end_date = NOW(), updated_at = NOW()
757 WHERE subscription_id = $1`,
758 [req.params.id]
759 );
760
761 // Log history
762 await pool.query(
763 `INSERT INTO office365_subscription_history
764 (subscription_id, tenant_id, action, previous_status, new_status, changed_by)
765 VALUES ($1, $2, 'cancelled', $3, 'cancelled', $4)`,
766 [req.params.id, req.tenant.id, subscription.status, req.user.id]
767 );
768
769 res.json({ message: 'Subscription cancelled successfully' });
770 } catch (err) {
771 console.error('Error cancelling subscription:', err);
772 res.status(500).json({ error: 'Failed to cancel subscription', details: err.message });
773 }
774});
775
776module.exports = router;