Skip to content

Conversation

@HiranyaKavishani
Copy link
Contributor

@HiranyaKavishani HiranyaKavishani commented Dec 23, 2025

Purpose

$Subject

Goals

Describe the solutions that this feature/fix will introduce to resolve the problems described above

Approach

Describe how you are implementing the solutions. Include an animated GIF or screenshot if the change affects the UI (email [email protected] to review all UI text). Include a link to a Markdown file or Google doc if the feature write-up is too long to paste here.

User stories

Summary of user stories addressed by this change>

Release note

Brief description of the new feature or bug fix as it will appear in the release notes

Documentation

Link(s) to product documentation that addresses the changes of this PR. If no doc impact, enter “N/A” plus brief explanation of why there’s no doc impact

Training

Link to the PR for changes to the training content in https://github.com/wso2/WSO2-Training, if applicable

Certification

Type “Sent” when you have provided new/updated certification questions, plus four answers for each question (correct answer highlighted in bold), based on this change. Certification questions/answers should be sent to [email protected] and NOT pasted in this PR. If there is no impact on certification exams, type “N/A” and explain why.

Marketing

Link to drafts of marketing content that will describe and promote this feature, including product page changes, technical articles, blog posts, videos, etc., if applicable

Automation tests

  • Unit tests

    Code coverage information

  • Integration tests

    Details about the test cases and coverage

Security checks

Samples

Provide high-level details about the samples related to this feature

Related PRs

List any other related PRs

Migrations (if applicable)

Describe migration steps and platforms on which migration has been tested

Test environment

List all JDK versions, operating systems, databases, and browser/versions on which this feature/fix was tested

Learning

Describe the research phase and any blog posts, patterns, libraries, or add-ons you used to solve the problem.

Summary by CodeRabbit

  • New Features

    • Added comprehensive billing dashboard for managing subscriptions, invoices, and payments.
    • Integrated Stripe payment processing for paid API subscription plans.
    • Introduced pricing support for subscription plans with flat-rate and per-unit billing models.
    • Added invoice management with download and hosted invoice viewing.
    • Enabled payment method management and customer billing portal access.
    • Introduced usage tracking and reporting for active subscriptions.
  • Chores

    • Updated dependencies including Stripe integration library.

✏️ Tip: You can customize this high-level summary in your review settings.

@coderabbitai
Copy link

coderabbitai bot commented Dec 23, 2025

Walkthrough

This pull request introduces a comprehensive billing and monetization system enabling API subscription payments via Stripe. Changes include database schema extensions for pricing and billing fields, new controllers and services for checkout and billing management, Stripe and Moesif integrations, encryption utilities for secure key storage, billing dashboard UI with usage and invoice tracking, and client-side scripts orchestrating checkout flows and payment workflows.

Changes

Cohort / File(s) Summary
Database Schema & Models
artifacts/script.sql, src/models/billingEngineKey.js, src/models/subscriptionPolicy.js, src/models/application.js, src/models/apiMetadata.js
Added DP_BILLING_ENGINE_KEYS table with encrypted key storage; extended DP_SUBSCRIPTION_POLICY with pricing fields (PRICING_MODEL, CURRENCY, BILLING_PERIOD, FLAT_AMOUNT, UNIT_AMOUNT, PRICING_METADATA); added billing fields to DP_API_SUBSCRIPTION (BILLING_CUSTOMER_ID, BILLING_SUBSCRIPTION_ID, PAYMENT_PROVIDER, PAYMENT_STATUS, CHECKOUT_SESSION_ID); added MONETIZATION_ENABLED flag to DP_API_METADATA; created three new indexes on DP_API_SUBSCRIPTION.
Billing Controllers & Request Handlers
src/controllers/billingController.js, src/controllers/invoiceController.js, src/controllers/usageController.js, src/controllers/authController.js
Introduced billingController with 13 endpoints for checkout, subscriptions, billing portals, and billing engine keys CRUD; added invoiceController for invoice retrieval and PDF/hosted links; added usageController for subscription usage; added renderBillingPage to authController.
Core Monetization & Payment Services
src/services/monetizationService.js, src/services/stripeService.js, src/services/moesifService.js, src/services/orgBillingKeyService.js
Implemented monetizationService orchestrating Stripe checkout, session registration, billing portals, and Moesif sync; built stripeService wrapping Stripe API with customer, subscription, and webhook handling; added moesifService for usage analytics and customer mapping; introduced orgBillingKeyService for encrypted key retrieval.
Data Access Layer
src/dao/admin.js, src/dao/apiMetadata.js
Extended admin.js with billing-related queries (updateBillingFields, findSubscriptionByUniqueKey, listSubscriptionsByOrg, getActiveSubscriptionsWithDetails, updateSubscriptionByBillingId, getAPIById, getSubscriptionPolicyById); filtered PENDING payments in subscription queries; refactored apiMetadata.js with pricing helpers and streamlined policy upsert logic.
Routes & Middleware
src/routes/devportalRoute.js, src/routes/authRoute.js, src/middlewares/billingAuth.js
Added devportal routes for billing, invoices, usage, and payment methods under /organizations/:orgId; integrated ensureBillingAuth middleware; added auth route for GET /:orgName/views/:viewName/billing and /billing/return Stripe return handler.
Billing Dashboard UI Templates
src/pages/billing/page.hbs, src/pages/billing/partials/billing-content.hbs, src/defaultContent/pages/billing/page.hbs, src/defaultContent/pages/billing/partials/billing-content.hbs
Created billing page templates with tabbed interface for Usage Summary, Invoices, and Payment Details; includes stat cards, period selectors, and data tables for displaying usage, invoices, payment methods, and active subscriptions.
Subscription & API UI Updates
src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs, src/defaultContent/pages/apis/partials/api-listing.hbs, src/defaultContent/pages/apis/partials/subscription-plans.hbs, src/defaultContent/partials/header.hbs, src/defaultContent/layout/main.hbs
Enhanced subscription plan cards with pricing display logic (FLAT vs PER_UNIT models, currency formatting); added app selection dropdown for authenticated users; integrated Stripe embedded checkout modal; added billing portal link to user menu; included Stripe.js script and modal infrastructure.
Client-Side Billing Scripts
src/scripts/billing.js, src/scripts/subscription.js, src/scripts/common.js
Introduced comprehensive billing.js with async data loading for usage, invoices, payment methods, and subscriptions; refactored subscription.js to support free and paid flows with Stripe embedded checkout integration, return handling, and modal orchestration; updated common.js selector for subscribe button element.
Supporting Utilities & Configuration
package.json, src/app.js, src/utils/cryptoUtil.js, src/utils/util.js, src/dto/apiDTO.js, src/dto/subscriptionPolicy.js, src/styles/billing.css
Added dotenv, stripe, and updated node-fetch dependencies; loaded env vars on startup; registered Handlebars compare and formatPrice helpers; added raw JSON middleware for /webhooks/stripe; implemented AES-256-CBC encryption for key storage; extended DTOs with monetization and pricing fields; created billing.css with responsive layout and animations.

Sequence Diagram(s)

sequenceDiagram
    actor User
    participant Client as Browser / Client
    participant DevPortal as DevPortal Server
    participant Stripe as Stripe API
    participant DB as Database
    participant Moesif as Moesif Service

    User->>Client: Click "Subscribe & Pay"
    Client->>DevPortal: POST /organizations/:orgId/monetization/checkout<br/>(apiId, policyId, applicationID)
    DevPortal->>DB: Check API, Policy, and create PENDING subscription
    DB-->>DevPortal: Subscription created (ID, status=PENDING)
    DevPortal->>Stripe: Create embedded checkout session<br/>(customerId, priceId, returnUrl, metadata)
    Stripe-->>DevPortal: { clientSecret, publishableKey, sessionId }
    DevPortal-->>Client: { subId, sessionId, clientSecret, publishableKey }
    Client->>Stripe: Stripe.js embedded checkout UI<br/>(clientSecret)
    User->>Stripe: Enter payment details
    Stripe-->>Client: Payment confirmed, redirect to returnUrl<br/>with session_id, org_id, dp_sub_id
    Client->>DevPortal: GET /billing/return<br/>(session_id, org_id, dp_sub_id)
    DevPortal->>Stripe: Verify checkout session<br/>(session_id)
    Stripe-->>DevPortal: Session details + subscription
    DevPortal->>DB: Update subscription<br/>(PAYMENT_STATUS=ACTIVE, billing IDs, CHECKOUT_SESSION_ID)
    DB-->>DevPortal: ✓
    DevPortal->>Moesif: Sync customer & subscription<br/>(companyId, userId, subscriptionId)
    Moesif-->>DevPortal: ✓ (non-blocking on error)
    DevPortal-->>Client: Redirect to success page
    Client-->>User: Show success message

    rect rgb(200, 220, 255)
    Note over DevPortal, DB: Billing Portal Flow
    User->>Client: Click "Manage Billing"
    Client->>DevPortal: POST /organizations/:orgId/billing-portal<br/>(returnUrl)
    DevPortal->>Stripe: Create customer portal session<br/>(customerId, returnUrl)
    Stripe-->>DevPortal: { url }
    DevPortal-->>Client: { url }
    Client->>Stripe: Redirect to customer portal
    User->>Stripe: Manage payment methods & subscriptions
    Stripe-->>Client: Redirect back to returnUrl
    end
Loading

Estimated code review effort

🎯 4 (Complex) | ⏱️ ~45 minutes

Poem

🐰 A hop toward commerce, with keys kept safe,
Stripe cards and Moesif metrics in place,
Checkouts now gleam with embedded delight,
Billing pages shine, subscriptions take flight!
From pending to active, the coins align—
A monetized platform? Simply divine!

Pre-merge checks and finishing touches

❌ Failed checks (2 warnings)
Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description is entirely a template with placeholder text; it contains no substantive information about the actual changes, goals, approach, testing, security checks, or any of the required sections beyond the template structure itself. Complete the PR description by replacing all placeholders with concrete details: explain the purpose and problems addressed, describe the implementation approach with UI artifacts if applicable, provide documentation/training links, confirm security checks, and detail testing performed.
Docstring Coverage ⚠️ Warning Docstring coverage is 62.60% which is insufficient. The required threshold is 80.00%. You can run @coderabbitai generate docstrings to improve docstring coverage.
✅ Passed checks (1 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly and concisely summarizes the main change: adding monetization support to the Developer Portal, which aligns with the extensive billing/payment infrastructure changes throughout the codebase.
✨ Finishing touches
  • 📝 Generate docstrings
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

Copy link

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Actionable comments posted: 14

Note

Due to the large number of review comments, Critical severity comments were prioritized as inline comments.

Caution

Some comments are outside the diff and can’t be posted inline due to platform limitations.

⚠️ Outside diff range comments (3)
src/scripts/common.js (3)

527-531: Same inconsistent selector pattern.

Line 527 uses .common-btn-primary while other updated sections use .subscribe-btn.


822-825: Same inconsistent selector pattern in subscription cards section.

Line 822 uses .common-btn-primary while line 648 and 736 in the same section use .subscribe-btn.


436-439: Standardize subscribe button selector to .subscribe-btn.

This code uses .common-btn-primary[disabled] (line 436), while other parts of the same file (lines 238, 309, 346, 648, 736) use .subscribe-btn. Both selectors work because the button has both classes in the templates (class="common-btn-primary subscribe-btn"), but the inconsistency creates unnecessary fragility and maintenance burden. Use .subscribe-btn throughout since it's more specific to the subscribe button purpose. Also update lines 527, 616, and 822 which use .common-btn-primary alone.

🟠 Major comments (9)
src/defaultContent/layout/main.hbs-15-26 (1)

15-26: Remove debug console.log statements before production release.

The Stripe load verification logs (console.log/console.error) are useful for development but should be removed or guarded behind a debug flag for production builds.

🔎 Proposed fix
     <script src="https://js.stripe.com/v3/"></script>
-    <script>
-        // Verify Stripe loads
-        window.addEventListener('load', function() {
-            if (window.Stripe) {
-                console.log('✅ Stripe.js loaded successfully');
-            } else {
-                console.error('❌ Stripe.js failed to load');
-            }
-        });
-    </script>
src/middlewares/billingAuth.js-28-31 (1)

28-31: Avoid logging PII (user email) even at debug level.

Logging userEmail may violate data privacy policies (GDPR/CCPA) and create compliance risks. Consider logging a non-PII identifier like a user ID hash or removing the email from logs entirely.

🔎 Proposed fix
     logger.debug('Billing auth check passed', { 
-        userEmail: req.user.email,
+        userId: req.user.id || req.user.sub,
         hasAccessToken: !!req.user.accessToken
     });
src/controllers/authController.js-266-272 (1)

266-272: Reconsider using orgName as orgId fallback.

When orgId lookup fails, the code falls back to using orgName as the orgId (line 271). This could lead to authorization bypass or data leakage if billing logic relies on the orgId being a valid UUID. Consider returning an error instead.

🔎 Proposed fix
         // Get orgId from the orgName in the URL
         let orgId;
         try {
             orgId = await adminDao.getOrgId(orgName);
+            if (!orgId) {
+                throw new Error('Organization not found');
+            }
         } catch (err) {
-            logger.error('Organization not found, using orgName as orgId', { orgName });
-            orgId = orgName; // Use orgName as fallback if org not in database
+            logger.error('Organization not found', { 
+                orgName, 
+                error: err.message 
+            });
+            return res.status(404).send('Organization not found');
         }
src/controllers/authController.js-252-314 (1)

252-314: Add input validation for route parameters.

The function should validate orgName and viewName parameters to prevent injection attacks or invalid data processing, similar to other handlers in this controller.

🔎 Proposed fix

Add validation at the beginning of the function:

 const renderBillingPage = async (req, res) => {
     try {
+        const rules = util.validateRequestParameters();
+        for (let validation of rules) {
+            await validation.run(req);
+        }
+        const errors = validationResult(req);
+        if (!errors.isEmpty()) {
+            return res.status(400).json(util.getErrors(errors));
+        }
+        
         const orgName = req.params.orgName;
         const viewName = req.params.viewName || 'default';

Committable suggestion skipped: line range outside the PR's diff.

src/services/orgBillingKeyService.js-14-14 (1)

14-14: Use a constant for billing engine name.

The hardcoded string 'stripe' should be replaced with a constant for maintainability and consistency across the codebase.

🔎 Proposed refactor
+const constants = require('../utils/constants');
+
 /**
  * Fetches and decrypts Stripe keys for a given orgId.
  * @param {string} orgId - The organization ID
  * @returns {Promise<{secretKey: string, publishableKey: string, webhookSecret: string}>}
  */
 async function getDecryptedStripeKeysForOrg(orgId) {
   if (!orgId) throw new Error('orgId is required to fetch Stripe keys');
   const record = await BillingEngineKey.findOne({
     where: {
       ORG_ID: orgId,
-      BILLING_ENGINE: 'stripe',
+      BILLING_ENGINE: constants.BILLING_ENGINE.STRIPE,
     },
   });

Committable suggestion skipped: line range outside the PR's diff.

src/services/orgBillingKeyService.js-9-23 (1)

9-23: Add error handling and audit logging for key decryption.

This function handles sensitive cryptographic operations and should include:

  1. Explicit error handling for decryption failures (e.g., corrupted data, wrong keys)
  2. Audit logging for security monitoring
  3. More specific error messages
🔎 Proposed improvements
+const logger = require('../config/logger');
+
 async function getDecryptedStripeKeysForOrg(orgId) {
   if (!orgId) throw new Error('orgId is required to fetch Stripe keys');
+  
+  logger.info('Fetching Stripe keys for organization', { orgId });
+  
   const record = await BillingEngineKey.findOne({
     where: {
       ORG_ID: orgId,
       BILLING_ENGINE: 'stripe',
     },
   });
-  if (!record) throw new Error('Stripe keys not found for this org');
+  
+  if (!record) {
+    logger.warn('Stripe keys not found for organization', { orgId });
+    throw new Error(`Stripe keys not found for organization: ${orgId}`);
+  }
+  
+  try {
-    return {
-      secretKey: decrypt(record.SECRET_KEY_ENC),
-      publishableKey: decrypt(record.PUBLISHABLE_KEY_ENC),
-      webhookSecret: decrypt(record.WEBHOOK_SECRET_ENC),
-    };
+    const keys = {
+      secretKey: decrypt(record.SECRET_KEY_ENC),
+      publishableKey: decrypt(record.PUBLISHABLE_KEY_ENC),
+      webhookSecret: decrypt(record.WEBHOOK_SECRET_ENC),
+    };
+    logger.info('Successfully decrypted Stripe keys', { orgId });
+    return keys;
+  } catch (error) {
+    logger.error('Failed to decrypt Stripe keys', { 
+      orgId, 
+      error: error.message 
+    });
+    throw new Error('Failed to decrypt Stripe keys for organization');
+  }
 }
src/defaultContent/pages/apis/partials/api-listing.hbs-207-236 (1)

207-236: Remove duplicate closing tag on line 236.

Line 236 contains concatenated closing tags {{/if}} {{/if}} which will cause rendering issues. The first {{/if}} correctly closes the condition from line 207, but the second is extraneous and should be removed. This appears to be a merge artifact.

src/scripts/subscription.js-71-92 (1)

71-92: Remove duplicate function definitions.

Functions hideElementById (lines 71-74 and 133-138), closeModal (lines 76-80 and 140-143), window.onclick (lines 82-87 and 145-150), and loadModal (lines 89-92 and 256-259) are defined twice. The later definitions override the earlier ones, potentially with different behavior.

Analysis

The first set of definitions (lines 71-92) appear to be new additions that are cleaner, while lines 133-259 contain the original implementations. Remove the duplicates and consolidate to a single definition of each function.

src/services/moesifService.js-334-343 (1)

334-343: Missing createMoesifSubscription from exports.

The function createMoesifSubscription is defined at lines 252-286 but is not included in module.exports. This function is likely used by monetizationService.js for subscription syncing.

Proposed fix
 module.exports = {
   getPlansFromMoesif,
   getCompany,
   extractSubscriptionsFromCompanyObject,
   getUsageForSubscription,
   syncCustomerMapping,
   createOrUpdateUser,
+  createMoesifSubscription,
   createBillingMeter,
   // (optional) expose app id if needed for ingestion
   getAppId,
 };
🟡 Minor comments (6)
package.json-38-38 (1)

38-38: Fix invalid dotenv version in dependencies.

The dotenv package version ^17.2.3 does not exist. The latest published version is 9.0.2. Update the dependency to a valid version.

src/routes/authRoute.js-23-23 (1)

23-23: Fix the misspelled middleware function name.

The middleware function enforceSecuirty is misspelled and should be enforceSecurity. Rename the function definition in src/middlewares/ensureAuthenticated.js (line 34) and update the export (line 449) and corresponding import in src/routes/authRoute.js (line 23) to use the correct spelling.

src/app.js-252-256 (1)

252-256: Potential NaN handling in formatPrice helper.

If price is a non-numeric string (e.g., "abc"), parseFloat(price) returns NaN, and NaN.toString() produces "NaN" which would be displayed in the UI.

🔎 Suggested defensive fix
 Handlebars.registerHelper('formatPrice', function (price) {
     if (!price) return '0';
-    // Convert to number and remove trailing zeros
-    return parseFloat(price).toString();
+    const parsed = parseFloat(price);
+    return isNaN(parsed) ? '0' : parsed.toString();
 });
src/pages/billing/partials/billing-content.hbs-204-206 (1)

204-206: Remove debug console.log statement.

The button onclick contains a debug console.log('🔵 Button clicked!') statement that should be removed before production.

🔎 Suggested fix
-                      <button class="common-btn-outlined" onclick="console.log('🔵 Button clicked!'); addPaymentMethod(); return false;">
+                      <button class="common-btn-outlined" onclick="addPaymentMethod()">
                         <i class="fas fa-credit-card me-2"></i>Manage in Stripe
                       </button>
src/controllers/usageController.js-37-47 (1)

37-47: Missing date validation in getRange.

If usageFrom or usageTo contain invalid date strings (e.g., "invalid"), new Date("invalid").toISOString() throws a RangeError: Invalid time value. Consider adding validation.

🔎 Suggested defensive fix
 function getRange(req) {
   const usageFrom = req.query.usageFrom;
   const usageTo = req.query.usageTo;

-  const to = usageTo ? new Date(usageTo).toISOString() : new Date().toISOString();
-  const from = usageFrom
-    ? new Date(usageFrom).toISOString()
-    : new Date(Date.now() - 30 * 24 * 60 * 60 * 1000).toISOString();
+  const now = new Date();
+  const defaultFrom = new Date(Date.now() - 30 * 24 * 60 * 60 * 1000);
+  
+  let to = now;
+  let from = defaultFrom;
+  
+  if (usageTo) {
+    const parsedTo = new Date(usageTo);
+    if (!isNaN(parsedTo.getTime())) {
+      to = parsedTo;
+    }
+  }
+  
+  if (usageFrom) {
+    const parsedFrom = new Date(usageFrom);
+    if (!isNaN(parsedFrom.getTime())) {
+      from = parsedFrom;
+    }
+  }

-  return { from, to };
+  return { from: from.toISOString(), to: to.toISOString() };
 }
src/services/stripeService.js-188-198 (1)

188-198: Wrap switch case variable declaration in a block.

The const subStatus declaration at line 190 inside the switch case can be erroneously accessed by other cases. Wrap in a block as suggested by static analysis.

Proposed fix
     case "customer.subscription.updated":
+    {
       // Check if subscription is past_due or unpaid
       const subStatus = event.data?.object?.status;
       if (subStatus === "past_due" || subStatus === "unpaid") {
         newStatus = PAYMENT_STATUS.PAST_DUE;
       } else if (subStatus === "active") {
         newStatus = PAYMENT_STATUS.ACTIVE;
       } else if (subStatus === "canceled") {
         newStatus = PAYMENT_STATUS.CANCELED;
       }
       break;
+    }
🧹 Nitpick comments (31)
src/utils/cryptoUtil.js (1)

14-21: Add error handling for malformed encrypted input.

The decrypt function will throw cryptic errors if the input doesn't contain a colon separator or contains invalid base64. Consider wrapping in try-catch with a clearer error message.

🔎 Proposed fix
 function decrypt(text) {
+  if (!text || !text.includes(':')) {
+    throw new Error('Invalid encrypted text format');
+  }
   const [ivStr, encrypted] = text.split(':');
+  if (!ivStr || !encrypted) {
+    throw new Error('Invalid encrypted text format');
+  }
   const iv = Buffer.from(ivStr, 'base64');
   const decipher = crypto.createDecipheriv('aes-256-cbc', Buffer.from(ENCRYPTION_KEY), iv);
   let decrypted = decipher.update(encrypted, 'base64', 'utf8');
   decrypted += decipher.final('utf8');
   return decrypted;
 }
src/models/subscriptionPolicy.js (1)

61-68: Consider specifying precision and scale for monetary DECIMAL fields.

For monetary values, it's best practice to explicitly define precision and scale to avoid unexpected rounding. Typical currency precision is (10, 2) for amounts up to 99,999,999.99.

🔎 Proposed enhancement
     FLAT_AMOUNT: {
-        type: DataTypes.DECIMAL,
+        type: DataTypes.DECIMAL(10, 2),
         allowNull: true
     },
     UNIT_AMOUNT: {
-        type: DataTypes.DECIMAL,
+        type: DataTypes.DECIMAL(10, 2),
         allowNull: true
     },
src/defaultContent/layout/main.hbs (2)

16-16: Consider lazy-loading Stripe.js only on billing pages.

Loading Stripe.js on every page adds ~30KB to the initial bundle and an external request, even for users who never visit billing. Consider loading it dynamically only when needed.


50-56: Extract inline styles to a CSS file for maintainability.

The modal contains extensive inline styles that should be moved to a stylesheet for consistency, maintainability, and separation of concerns. Consider adding modal-related styles to a dedicated CSS file (e.g., src/defaultContent/styles/modal.css or within an existing stylesheet like main.css). Note that closeStripeCheckoutModal() is already properly defined in src/scripts/subscription.js and exposed to the window object.

src/pages/layout/main.hbs (1)

13-24: Duplicate code across layout files creates maintenance burden.

The Stripe.js integration and modal are duplicated in both src/defaultContent/layout/main.hbs and src/pages/layout/main.hbs. Consider extracting to a shared partial (e.g., {{> stripe-checkout-modal }}) to ensure consistency and easier maintenance.

The same issues noted for the other layout file apply here:

  • Remove debug console.log statements before production
  • Extract inline styles to CSS
  • Consider lazy-loading Stripe.js

Also applies to: 31-39

src/defaultContent/pages/billing/page.hbs (1)

38-72: Reduce production logging verbosity.

While the script loading mechanism is sound, the extensive console logging (lines 39-40, 46, 54-60, 63-67, 71) is excessive for production. Consider wrapping debug logs in a development-mode check or removing non-essential logs.

🔎 Proposed refactor
 <script>
-    console.log('📄 Billing page template rendered - ' + new Date().toISOString());
-    console.log('🌐 Current URL:', window.location.href);
-    
     // Clear any cached scripts
     var cacheBuster = '?v=' + Date.now();
     var scriptUrl = window.location.origin + '/technical-scripts/billing.js' + cacheBuster;
     
-    console.log('🔍 Loading billing.js from:', scriptUrl);
-    
     // Dynamically load the script to avoid path and caching issues
     var script = document.createElement('script');
     script.src = scriptUrl;
     script.type = 'text/javascript';
     script.async = false;
     
     script.onload = function() {
-        console.log('✅ billing.js loaded and executed successfully!');
-        console.log('✅ Window functions exposed:', {
-            loadUsageData: typeof window.loadUsageData,
-            loadInvoices: typeof window.loadInvoices,
-            loadPaymentDetails: typeof window.loadPaymentDetails
-        });
+        // Script loaded successfully
     };
     
     script.onerror = function(error) {
         console.error('❌ FAILED to load billing.js');
-        console.error('❌ Attempted URL:', scriptUrl);
-        console.error('❌ Error:', error);
         alert('Failed to load billing.js from: ' + scriptUrl);
     };
     
     document.body.appendChild(script);
-    console.log('📌 Script tag appended to body');
 </script>
src/pages/billing/page.hbs (2)

28-54: Reduce production console logging.

Similar to the defaultContent version, this template contains excessive console logging. Consider keeping only error logs and removing debug-level logs for production.

🔎 Proposed refactor
 <script>
-    console.log('📄 Billing page rendered from src/pages/billing/page.hbs');
-    console.log('🌐 Current URL:', window.location.href);
-    console.log('🔍 orgId:', '{{orgId}}');
-    
     // Load billing.js with cache busting
     var cacheBuster = '?v=' + Date.now();
     var scriptUrl = window.location.origin + '/technical-scripts/billing.js' + cacheBuster;
     
-    console.log('🔍 Loading billing.js from:', scriptUrl);
-    
     var script = document.createElement('script');
     script.src = scriptUrl;
     script.type = 'text/javascript';
     script.async = false;
     
     script.onload = function() {
-        console.log('✅ billing.js loaded and executed successfully!');
+        // Script loaded successfully
     };
     
     script.onerror = function(error) {
         console.error('❌ FAILED to load billing.js');
-        console.error('❌ Attempted URL:', scriptUrl);
     };
     
     document.body.appendChild(script);
 </script>

1-54: Consider consolidating duplicate billing page templates.

Two nearly identical billing page templates exist: src/defaultContent/pages/billing/page.hbs and src/pages/billing/page.hbs. This duplication increases maintenance burden and the risk of inconsistencies.

Consider consolidating these into a single template or clearly documenting when each should be used.

src/models/billingEngineKey.js (1)

30-37: Consider enabling Sequelize automatic timestamps.

The model has manual CREATED_AT and UPDATED_AT fields but sets timestamps: false. For consistency and to leverage Sequelize's automatic timestamp management, consider enabling timestamps: true and using the standard createdAt/updatedAt naming or configuring custom field names.

🔎 Alternative approach
   CREATED_AT: {
     type: DataTypes.DATE,
-    defaultValue: DataTypes.NOW
+    field: 'CREATED_AT'
   },
   UPDATED_AT: {
     type: DataTypes.DATE,
-    defaultValue: DataTypes.NOW
+    field: 'UPDATED_AT'
   }
 }, {
   tableName: 'DP_BILLING_ENGINE_KEYS',
   indexes: [
     { fields: ['ORG_ID', 'BILLING_ENGINE'], unique: true }
   ],
-  timestamps: false
+  timestamps: true,
+  createdAt: 'CREATED_AT',
+  updatedAt: 'UPDATED_AT'
 });

Also applies to: 43-43

src/controllers/authController.js (2)

252-263: Remove or reduce debug logging.

The debug logging block (lines 258-263) exposes potentially sensitive session information. Consider removing this or wrapping it in a development-mode check.

🔎 Proposed refactor
 const renderBillingPage = async (req, res) => {
     try {
         const orgName = req.params.orgName;
         const viewName = req.params.viewName || 'default';
         
-        // Debug: Log user session info
-        logger.info('Rendering billing page', {
-            hasUser: !!req.user,
-            hasAccessToken: !!req.user?.accessToken,
-            userEmail: req.user?.email,
-            isAuthenticated: req.isAuthenticated ? req.isAuthenticated() : false
-        });
-        
         // Get orgId from the orgName in the URL

302-312: Avoid exposing error details in production.

The error handler exposes the full error object in developer mode (line 304). While useful for debugging, consider using a more controlled approach that doesn't leak implementation details.

🔎 Proposed improvement
         const errorContent = {
             message: 'Failed to load billing page',
-            error: config.devportalMode === 'developer' ? error : {}
+            error: (config.devportalMode === 'developer' && config.mode === 'development') 
+                ? { message: error.message, stack: error.stack } 
+                : {}
         };
src/styles/billing.css (2)

204-218: Overlapping media queries with redundant rules.

The breakpoints at max-width: 767px (line 204) and max-width: 768px (line 214) are nearly identical and both set the same margin-bottom: 16px for .stat-card. Consider consolidating these into a single media query.

🔎 Suggested consolidation
 @media (max-width: 767px) {
   .stat-card {
     margin-bottom: 16px;
   }
   
   .stat-cards-row {
     gap: 12px;
   }
 }
-
-@media (max-width: 768px) {
-  .stat-card {
-    margin-bottom: 16px;
-  }
-}

46-75: Hardcoded colors mixed with CSS variables.

The billing tabs use hardcoded colors (e.g., #6c757d, #0d6efd) while other parts of the stylesheet reference CSS variables like var(--primary-main-color). For consistency and easier theming, consider using CSS variables for these colors as well.

src/pages/billing/partials/billing-content.hbs (1)

67-113: Consider using responsive grid classes for stat cards.

The stat cards use fixed col-md-3 which may cause layout issues on smaller screens. The defaultContent version uses more responsive classes (col-xl-3 col-lg-3 col-md-6 col-sm-6). Consider aligning both templates for consistency.

src/models/application.js (1)

147-152: Consider using ENUM for PAYMENT_STATUS to enforce valid values.

The PAYMENT_STATUS field accepts any string, but based on typical billing workflows, it likely has a finite set of valid states (e.g., 'ACTIVE', 'PENDING', 'CANCELED', 'PAST_DUE'). Using an ENUM, similar to TOKEN_TYPE on line 102-104, would provide data integrity at the model level.

🔎 Proposed fix
-    PAYMENT_STATUS: { type: DataTypes.STRING, allowNull: false, defaultValue: 'ACTIVE' },
+    PAYMENT_STATUS: { 
+        type: DataTypes.ENUM,
+        values: ['PENDING', 'ACTIVE', 'CANCELED', 'PAST_DUE', 'INCOMPLETE'],
+        allowNull: false, 
+        defaultValue: 'ACTIVE' 
+    },
src/controllers/invoiceController.js (2)

19-19: Misleading variable name: subscriptionService for monetizationService.

The import names the module subscriptionService but it's actually monetizationService. This inconsistency could cause confusion during maintenance.

🔎 Proposed fix
-const subscriptionService = require("../services/monetizationService");
+const monetizationService = require("../services/monetizationService");

Then update all usages from subscriptionService.* to monetizationService.*.


24-35: errorToResponse is duplicated across controllers.

This utility function is identical in usageController.js (line 57) and billingController.js (line 106). Consider extracting it to a shared utility module to reduce duplication.

src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs (1)

156-156: Clarify the empty string parameter in subscribe() calls.

Both free subscription handlers pass an empty string as the second argument: subscribe('{{@root.orgID}}', '', ...). If this represents an optional appId, consider using a named constant or documenting the parameter's purpose for maintainability.

Also applies to: 170-170

src/routes/devportalRoute.js (1)

192-193: Remove commented-out code.

The TODO comment indicates devportalController.login doesn't exist. Either implement the route or remove this dead code entirely rather than leaving it commented.

src/services/moesifService.js (3)

55-59: Log or include fetched error text in the exception.

The error text is fetched at line 56 but never used. Either log it for debugging purposes or include it in the error details.

Proposed fix
   if (!res.ok) {
     const text = await res.text();
-    throw new BadRequestError("Failed to fetch plans from Moesif", { status: res.status });
+    logger.error({ status: res.status, text }, "Failed to fetch plans from Moesif");
+    throw new BadRequestError("Failed to fetch plans from Moesif", { status: res.status, detail: text });
   }

121-124: Same pattern: fetched error text is unused.

This occurs in multiple places (lines 56, 68, 122, 169, 195, 231, 275, 321). Consider a helper function to consistently handle Moesif API errors.


236-238: Remove redundant catch-rethrow block.

The catch block at lines 236-238 only rethrows the error without any additional handling. This is unnecessary.

Proposed fix
-  try {
     const res = await fetch(`https://api.moesif.com/v1/search/~/users/${encodeURIComponent(userId)}`, {
       method: "POST",
       headers: {
         Authorization: `Bearer ${token}`,
         "Content-Type": "application/json",
       },
       body: JSON.stringify(userPayload),
     });
     
     if (!res.ok) {
       const text = await res.text();
       throw new BadRequestError("Failed to create/update user in Moesif", { status: res.status });
     }
     
     return res.json();
-  } catch (e) {
-    throw e;
-  }
src/dao/admin.js (2)

880-903: Move sequelize require to top-level imports.

Requiring sequelize inside the function body at line 880 is unusual. While functional, it's better to import at the module level for consistency and slight performance improvement.

Proposed fix

Add at top of file (around line 19):

const sequelize = require('../db/sequelize');

Then remove line 880 from the function.


1093-1096: Consider using PAYMENT_STATUS constants instead of hardcoded string.

The filter at line 1095 correctly uses 'ACTIVE' (which matches the database default value), but it's inconsistent with the codebase pattern. Constants for PAYMENT_STATUS are defined in src/services/monetizationService.js (PAYMENT_STATUS.ACTIVE = "ACTIVE") and src/services/stripeService.js. For maintainability and consistency, import and use these constants instead of the hardcoded string 'ACTIVE'.

src/scripts/billing.js (2)

19-61: Remove or conditionally disable debug console.log statements.

Extensive debug logging (lines 19, 23-24, 28, 31-34, 36, 43, 48, 60, etc.) throughout the file should be removed or wrapped in a development flag before production deployment.


729-738: Avoid falling back to 'default' orgId.

Falling back to 'default' when orgId is not found (line 737) could lead to unexpected behavior or security issues. Consider showing an error message instead.

Proposed fix
   // Fallback: try to extract from URL path (e.g., /hiranyakavi/views/default/billing)
   const pathParts = window.location.pathname.split('/');
   if (pathParts.length > 1 && pathParts[1]) {
     console.log('Using orgId from URL path:', pathParts[1]);
     return pathParts[1];
   }
   
-  console.warn('No orgId found, using default');
-  return 'default';
+  console.error('No orgId found in page context or URL');
+  return null;
src/scripts/subscription.js (2)

767-772: Avoid hardcoded external URL for subscription icon.

The subscription icon URL at line 767 points to an external GitHub raw content URL. This creates a dependency on external availability and may break if the repository changes.

Proposed fix

Host the icon locally within the project assets:

-        const subscriptionIconHtml = `<img src="https://raw.githubusercontent.com/wso2/docs-bijira/refs/heads/main/en/devportal-theming/success-rounded.svg"
+        const subscriptionIconHtml = `<img src="/assets/icons/success-rounded.svg"
           alt="Subscribed" class="subscription-icon" style="display: inline-block;" />`;

394-486: Remove extensive debug logging before production.

The openStripeEmbeddedCheckout function contains ~20 console.log statements for debugging. Consider removing these or using a configurable logger.

src/controllers/billingController.js (2)

81-91: Use logger consistently instead of mixing console.log.

The controller mixes console.log (lines 81, 108, 244, 254, 260-261, 266, 273, 284, 288, 295-296, 300) with structured logger calls. Use the logger consistently for production code.


336-337: Move require statements to top-level imports.

Lines 336-337, 361-362, 387, 408-409 use inline require() calls. Move these to the top of the file for clarity and slight performance improvement.

src/services/monetizationService.js (1)

357-362: Use logger instead of console.error for Moesif integration failures.

Lines 357-362 use console.log and console.error for Moesif integration logging. Use the structured logger for consistency.

📜 Review details

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

📥 Commits

Reviewing files that changed from the base of the PR and between d8e1cbf and e52f846.

⛔ Files ignored due to path filters (1)
  • package-lock.json is excluded by !**/package-lock.json
📒 Files selected for processing (40)
  • artifacts/script.sql
  • package.json
  • src/app.js
  • src/controllers/apiContentController.js
  • src/controllers/authController.js
  • src/controllers/billingController.js
  • src/controllers/invoiceController.js
  • src/controllers/usageController.js
  • src/dao/admin.js
  • src/dao/apiMetadata.js
  • src/defaultContent/layout/main.hbs
  • src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs
  • src/defaultContent/pages/apis/partials/api-listing.hbs
  • src/defaultContent/pages/apis/partials/subscription-plans.hbs
  • src/defaultContent/pages/billing/page.hbs
  • src/defaultContent/pages/billing/partials/billing-content.hbs
  • src/defaultContent/partials/header.hbs
  • src/dto/apiDTO.js
  • src/dto/subscriptionPolicy.js
  • src/middlewares/billingAuth.js
  • src/models/apiMetadata.js
  • src/models/application.js
  • src/models/billingEngineKey.js
  • src/models/subscriptionPolicy.js
  • src/pages/billing/page.hbs
  • src/pages/billing/partials/billing-content.hbs
  • src/pages/layout/main.hbs
  • src/routes/authRoute.js
  • src/routes/devportalRoute.js
  • src/scripts/billing.js
  • src/scripts/common.js
  • src/scripts/subscription.js
  • src/services/adminService.js
  • src/services/moesifService.js
  • src/services/monetizationService.js
  • src/services/orgBillingKeyService.js
  • src/services/stripeService.js
  • src/styles/billing.css
  • src/utils/cryptoUtil.js
  • src/utils/util.js
🧰 Additional context used
🧬 Code graph analysis (16)
src/models/billingEngineKey.js (7)
src/dao/admin.js (1)
  • sequelize (880-880)
src/models/apiMetadata.js (1)
  • sequelize (19-19)
src/models/application.js (1)
  • sequelize (19-19)
src/models/subscriptionPolicy.js (1)
  • sequelize (19-19)
src/services/adminService.js (1)
  • sequelize (29-29)
src/services/monetizationService.js (1)
  • sequelize (20-20)
src/services/orgBillingKeyService.js (1)
  • BillingEngineKey (1-1)
src/routes/authRoute.js (3)
src/routes/devportalRoute.js (4)
  • billingController (25-25)
  • require (28-28)
  • require (32-32)
  • constants (33-33)
src/middlewares/ensureAuthenticated.js (4)
  • require (24-24)
  • require (25-25)
  • require (27-27)
  • constants (20-20)
src/controllers/billingController.js (1)
  • constants (24-24)
src/controllers/authController.js (6)
src/controllers/billingController.js (3)
  • orgName (283-283)
  • adminDao (275-275)
  • util (25-25)
src/dao/admin.js (1)
  • logger (27-27)
src/utils/util.js (4)
  • logger (23-23)
  • adminDao (25-25)
  • constants (26-26)
  • config (31-31)
src/services/monetizationService.js (1)
  • adminDao (43-43)
src/models/application.js (1)
  • constants (22-22)
src/routes/authRoute.js (1)
  • constants (24-24)
src/utils/util.js (1)
src/scripts/api-key-generation.js (1)
  • subscriptionPlan (62-62)
src/middlewares/billingAuth.js (2)
src/app.js (4)
  • logger (33-33)
  • require (24-24)
  • require (28-28)
  • require (34-34)
src/controllers/billingController.js (14)
  • logger (23-23)
  • require (22-22)
  • require (336-336)
  • require (361-361)
  • require (409-409)
  • req (72-72)
  • req (242-242)
  • req (332-332)
  • req (357-357)
  • req (432-432)
  • req (453-453)
  • req (473-473)
  • req (493-493)
  • req (512-512)
src/controllers/invoiceController.js (2)
src/controllers/usageController.js (1)
  • errorToResponse (58-58)
src/controllers/billingController.js (6)
  • errorToResponse (107-107)
  • errorToResponse (141-141)
  • errorToResponse (159-159)
  • errorToResponse (176-176)
  • errorToResponse (197-197)
  • errorToResponse (217-217)
src/utils/cryptoUtil.js (1)
src/controllers/billingController.js (4)
  • require (22-22)
  • require (336-336)
  • require (361-361)
  • require (409-409)
src/controllers/usageController.js (2)
src/controllers/invoiceController.js (8)
  • subscriptionService (19-19)
  • require (20-20)
  • logger (21-21)
  • errorToResponse (45-45)
  • errorToResponse (59-59)
  • errorToResponse (72-72)
  • errorToResponse (90-90)
  • errorToResponse (104-104)
src/services/monetizationService.js (2)
  • logger (22-22)
  • usage (626-630)
src/scripts/common.js (1)
src/scripts/subscription.js (6)
  • subscribeButton (271-271)
  • subscribeButton (605-605)
  • subscribeButton (852-852)
  • card (270-270)
  • card (604-604)
  • card (850-850)
src/dto/subscriptionPolicy.js (1)
src/services/apiMetadataService.js (2)
  • subscriptionPolicy (768-768)
  • subscriptionPolicy (869-869)
src/services/orgBillingKeyService.js (1)
src/models/billingEngineKey.js (1)
  • BillingEngineKey (4-44)
src/dao/apiMetadata.js (2)
src/models/apiSubscriptionPolicy.js (2)
  • APISubscriptionPolicy (23-38)
  • SubscriptionPolicy (20-20)
src/models/subscriptionPolicy.js (1)
  • SubscriptionPolicy (22-77)
src/routes/devportalRoute.js (1)
src/routes/authRoute.js (2)
  • billingController (21-21)
  • router (19-19)
src/dao/admin.js (1)
src/models/application.js (1)
  • SubscriptionMapping (113-158)
src/services/stripeService.js (2)
src/services/monetizationService.js (6)
  • stripeSecretKey (426-426)
  • stripeSecretKey (451-451)
  • stripeSecretKey (460-460)
  • stripeSecretKey (476-476)
  • stripeSecretKey (497-497)
  • stripeSecretKey (533-533)
src/dao/admin.js (1)
  • getSubscription (709-724)
src/controllers/billingController.js (2)
src/dao/admin.js (5)
  • require (18-18)
  • require (19-19)
  • require (20-20)
  • require (21-21)
  • logger (27-27)
src/models/billingEngineKey.js (1)
  • BillingEngineKey (4-44)
🪛 Biome (2.1.2)
src/services/stripeService.js

[error] 201-201: Illegal return statement outside of a function

(parse)


[error] 205-205: Illegal return statement outside of a function

(parse)


[error] 190-190: Other switch clauses can erroneously access this declaration.
Wrap the declaration in a block to restrict its access to the switch clause.

The declaration is defined in this switch clause:

Safe fix: Wrap the declaration in a block.

(lint/correctness/noSwitchDeclarations)

🔇 Additional comments (35)
package.json (2)

58-58: Downgrade from node-fetch v3 to v2 for CommonJS compatibility.

This change from v3.x (ESM-only) to v2.x (CommonJS) is a common pattern for Node.js projects that haven't migrated to ES modules. Ensure this is intentional and that all fetch usages are compatible with v2 API.


74-74: Stripe package version is valid and up-to-date. The version ^20.0.0 resolves to [email protected] (or potentially 20.1.0), which is an active, non-deprecated release in the npm registry. No deprecation warnings are present. Consider monitoring for updates as the latest version in the 20.x line is currently 20.1.0.

src/defaultContent/partials/header.hbs (2)

12-12: Good fix: Properly quoted image src attributes.

The change from src={{profile.imageURL}} to src="{{profile.imageURL}}" ensures proper HTML attribute syntax and prevents potential parsing issues with special characters in URLs.

Also applies to: 18-18


28-31: LGTM: Billing menu item added to authenticated user dropdown.

The new Billing link is properly placed within the authenticated user context ({{#if profile}}) and follows the same pattern as the existing logout item.

src/middlewares/billingAuth.js (1)

25-45: Middleware logic is sound with proper defensive checks.

The guard req.isAuthenticated && req.isAuthenticated() correctly handles cases where isAuthenticated might not be defined on the request object.

src/services/adminService.js (2)

1204-1214: LGTM: Billing data extraction for paid subscriptions.

The code correctly checks for both billing fields before constructing the billingData object, ensuring incomplete billing info isn't propagated.


1396-1418: LGTM: Optional billing metadata propagation to control plane.

The billingMetadata is conditionally added to the subscription request only when billing data is present. Note that the 409 conflict fallback (lines 1421-1423) returns the existing subscription without updating billing metadata - verify this is the intended behavior for re-subscriptions.

src/models/subscriptionPolicy.js (1)

49-72: LGTM: New pricing-related fields for monetization support.

The new fields appropriately model flexible pricing configurations with JSONB for extensible metadata. All fields being nullable ensures backward compatibility with existing subscription policies.

src/utils/util.js (1)

809-814: LGTM - Subscription plan details enriched with pricing information.

The additional pricing fields (pricingModel, currency, billingPeriod, flatAmount, unitAmount, pricingMetadata) properly extend subscription plan objects to support the new monetization features. The implementation correctly propagates these fields from the loaded subscription plan.

src/models/billingEngineKey.js (1)

4-44: LGTM - Billing key model properly structured.

The BillingEngineKey model correctly defines:

  • Encrypted key storage with _ENC suffix naming
  • Unique constraint on (ORG_ID, BILLING_ENGINE) to prevent duplicate entries
  • Manual timestamp fields
src/routes/authRoute.js (1)

21-21: The billingController import is actively used on line 28.

The import is correctly utilized for the /billing/return route handler (billingController.handleBillingReturn). The controller is properly implemented with multiple billing endpoints (checkout sessions, subscriptions, billing portals, payment methods, etc.), so the single usage in this route file is intentional and appropriate.

src/models/apiMetadata.js (1)

106-110: LGTM - Monetization flag added correctly.

The MONETIZATION_ENABLED field is properly defined as a nullable boolean with a default value of false in both the Sequelize model and the database schema (artifacts/script.sql). This allows incremental adoption of monetization features per API.

src/app.js (2)

276-277: LGTM! Stripe webhook raw body parsing is correctly positioned.

The raw body parser for /webhooks/stripe is placed before express.json(), which is required for Stripe signature verification to work correctly.


155-191: LGTM! Well-implemented compare helper.

The helper properly validates argument count, handles all common comparison operators, and throws informative errors for unsupported operators.

src/defaultContent/pages/apis/partials/subscription-plans.hbs (2)

79-93: Verify pricing model fallback logic.

The {{else}} block of {{#compare pricingModel "===" "FLAT"}} at line 85 handles all non-FLAT pricing as PER_UNIT. If additional pricing models are added in the future (e.g., TIERED), they would incorrectly render as PER_UNIT pricing. Consider explicitly checking for PER_UNIT or adding a comment clarifying this is intentional.


112-123: LGTM! Subscribe button data attributes are well-structured.

The button correctly exposes data-policy-id, data-policy-name, data-external-price-id, and data-is-paid attributes for the client-side subscription flow.

src/defaultContent/pages/billing/partials/billing-content.hbs (2)

27-43: LGTM! Well-structured accessible tab navigation.

The tab navigation uses proper ARIA attributes (role="tablist", role="presentation", aria-controls, aria-selected) for accessibility compliance.


66-112: LGTM! Stat cards with appropriate loading states.

The stat cards layout is responsive and includes placeholder values (--) that will be populated dynamically. The loading states in the usage table provide good UX feedback.

src/controllers/apiContentController.js (2)

94-102: LGTM! Subscription policy augmentation is well-implemented.

The code correctly populates subscriptionPolicy with policyId and policyName when a subscription exists, with a sensible fallback to 'Unknown' if the policy is not found in the metadata.


358-366: LGTM! Consistent implementation with loadAPIs.

The same subscription policy augmentation pattern is correctly applied in loadAPIContent, maintaining consistency across both code paths.

src/controllers/usageController.js (2)

24-35: LGTM! Error handling follows established pattern.

The errorToResponse helper correctly handles CustomError instances and provides a sensible fallback for unexpected errors, matching the pattern used in invoiceController.js.


57-60: No action required — logger format is consistent.

The logger.error({ err, orgId, subId }, "message") format in usageController.js is consistent with related billing and usage files (invoiceController.js, billingController.js, monetizationService.js). This format is the established convention for the billing/usage domain in the codebase.

src/dto/apiDTO.js (2)

29-33: LGTM!

The monetization info initialization correctly defaults to false when MONETIZATION_ENABLED is undefined or falsy.


93-102: LGTM!

The pricing fields are properly mapped from the subscription policy object, maintaining consistency with the SubscriptionPolicy DTO pattern.

src/dto/subscriptionPolicy.js (1)

28-35: LGTM!

The pricing field mappings are consistent with the APISubscriptionPolicy class and align with the database schema additions.

src/defaultContent/pages/apis/partials/api-listing.hbs (1)

103-139: LGTM!

The pricing chips UI properly handles FLAT vs PER_UNIT pricing models with appropriate fallbacks to "Free" when amounts are missing.

src/dao/apiMetadata.js (4)

430-442: LGTM!

The toUpper and computeRequestCount helpers properly handle edge cases with null/undefined inputs and correctly normalize request/event count values.


444-496: Good refactoring to centralize subscription policy row construction.

The buildPricingMetadata and buildSubscriptionPolicyRow helpers consolidate previously duplicated logic. Minor note: on line 488, toUpper(policy.pricingModel) ?? null is slightly redundant since toUpper already returns null for falsy inputs, but this doesn't affect correctness.


498-510: LGTM!

The refactored createAPISubscriptionPolicy correctly supports both policyId and policyID naming conventions for API compatibility.


562-583: LGTM!

Properly removes ORG_ID and POLICY_ID from update payload to prevent modifying primary keys. The returning: true option ensures the updated row is returned.

src/controllers/invoiceController.js (1)

96-108: Verify hosted_invoice_url origin before redirecting.

The redirect uses the URL returned from Stripe without validation. While Stripe-returned URLs should be trusted, consider validating that the URL hostname matches expected Stripe domains (*.stripe.com) as a defense-in-depth measure against potential data corruption or injection.

src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs (1)

15-59: LGTM!

The pricing display logic correctly handles FLAT vs PER_UNIT models with appropriate fallbacks and clear visual layout for price and rate limit information.

src/routes/devportalRoute.js (1)

161-190: Well-structured billing routes with appropriate middleware.

The new billing routes are properly organized under /organizations/:orgId paths with ensureBillingAuth middleware. The Stripe webhook at line 190 correctly omits authentication since webhook verification is handled via Stripe signature in the controller.

src/dao/admin.js (1)

793-828: Consider using Sequelize's update with direct field mapping.

The updateBillingFields function manually checks each field. This is safe but verbose. Consider a whitelist-based approach for maintainability.

src/scripts/billing.js (1)

512-538: I am ready to rewrite a review comment, but I need you to provide the review comment that requires verification and rewriting. Please share the review comment within <review_comment> tags, along with any relevant context about the code being reviewed.

Comment on lines +267 to +271
CREATE INDEX "IDX_DP_SUB_ORG_APP_API" ON "DP_API_SUBSCRIPTION" ("ORG_ID","APP_ID","API_ID");

CREATE INDEX "IDX_DP_SUB_BILLING_SUB" ON "DP_API_SUBSCRIPTION" ("BILLING_SUBSCRIPTION_ID");

CREATE INDEX "IDX_DP_SUB_BILLING_CUSTOMER" ON "DP_API_SUBSCRIPTION" ("BILLING_CUSTOMER_ID");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Indexes reference columns that don't exist yet in CREATE TABLE.

The indexes IDX_DP_SUB_BILLING_SUB and IDX_DP_SUB_BILLING_CUSTOMER reference BILLING_SUBSCRIPTION_ID and BILLING_CUSTOMER_ID columns, but these columns are only added via ALTER TABLE statements at lines 296-300. The CREATE TABLE for DP_API_SUBSCRIPTION (lines 208-218) doesn't include these columns.

For a fresh database install, this script will fail because the indexes are created before the columns exist.

🔎 Proposed fix

Move the index creation statements after the ALTER TABLE statements, or add the columns directly in the CREATE TABLE statement:

 CREATE TABLE "DP_API_SUBSCRIPTION" (
     "SUB_ID" VARCHAR(256) NOT NULL,
     "APP_ID" VARCHAR(256) NOT NULL,
     "ORG_ID" VARCHAR(256) NOT NULL,
     "API_ID" VARCHAR(256) NOT NULL,
     "POLICY_ID" VARCHAR(256) NOT NULL,
+    "BILLING_CUSTOMER_ID" VARCHAR(256),
+    "BILLING_SUBSCRIPTION_ID" VARCHAR(256),
+    "PAYMENT_PROVIDER" VARCHAR(64),
+    "PAYMENT_STATUS" VARCHAR(64),
+    "CHECKOUT_SESSION_ID" VARCHAR(256),
     PRIMARY KEY ("SUB_ID"),
     ...
 );

Then remove the corresponding ALTER TABLE statements at lines 296-300.

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In artifacts/script.sql around lines 267 to 271, the two index statements
IDX_DP_SUB_BILLING_SUB and IDX_DP_SUB_BILLING_CUSTOMER reference
BILLING_SUBSCRIPTION_ID and BILLING_CUSTOMER_ID which are not present in the
CREATE TABLE for DP_API_SUBSCRIPTION (lines ~208-218) and are only added later
via ALTER TABLE (lines ~296-300); to fix, either move these CREATE INDEX
statements so they appear after the ALTER TABLE block that adds those columns,
or instead add BILLING_SUBSCRIPTION_ID and BILLING_CUSTOMER_ID directly into the
DP_API_SUBSCRIPTION CREATE TABLE and then remove the later ALTER TABLE
statements; ensure index creation happens only after the columns exist.

Comment on lines +273 to +285
-- DP_BILLING_ENGINE_KEYS: Billing engine keys for each org (Stripe, etc.)
CREATE TABLE IF NOT EXISTS "DP_BILLING_ENGINE_KEYS" (
"ID" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
"ORG_ID" UUID NOT NULL,
"BILLING_ENGINE" VARCHAR(32) NOT NULL,
"SECRET_KEY_ENC" TEXT NOT NULL,
"PUBLISHABLE_KEY_ENC" TEXT NOT NULL,
"WEBHOOK_SECRET_ENC" TEXT NOT NULL,
"CREATED_AT" TIMESTAMP DEFAULT NOW(),
"UPDATED_AT" TIMESTAMP DEFAULT NOW(),
UNIQUE ("ORG_ID", "BILLING_ENGINE")
);
CREATE INDEX IF NOT EXISTS "IDX_BILLING_ENGINE_KEYS_ORG_ID" ON "DP_BILLING_ENGINE_KEYS"("ORG_ID");
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Type mismatch: ORG_ID is UUID here but VARCHAR(256) in DP_ORGANIZATION.

DP_BILLING_ENGINE_KEYS defines ORG_ID as UUID NOT NULL, but DP_ORGANIZATION (line 24) uses VARCHAR(256) for its primary key ORG_ID. This type mismatch will prevent establishing a proper foreign key relationship and may cause join/lookup issues.

🔎 Proposed fix
 CREATE TABLE IF NOT EXISTS "DP_BILLING_ENGINE_KEYS" (
     "ID" UUID PRIMARY KEY DEFAULT gen_random_uuid(),
-    "ORG_ID" UUID NOT NULL,
+    "ORG_ID" VARCHAR(256) NOT NULL,
     "BILLING_ENGINE" VARCHAR(32) NOT NULL,
     ...
+    FOREIGN KEY ("ORG_ID") REFERENCES "DP_ORGANIZATION"("ORG_ID") ON DELETE CASCADE
 );

Committable suggestion skipped: line range outside the PR's diff.

🤖 Prompt for AI Agents
In artifacts/script.sql around lines 273 to 285, DP_BILLING_ENGINE_KEYS defines
ORG_ID as UUID while DP_ORGANIZATION (line 24) defines ORG_ID as VARCHAR(256);
change DP_BILLING_ENGINE_KEYS.ORG_ID to VARCHAR(256) to match the organization
table (or alternatively change DP_ORGANIZATION to UUID if you intend UUIDs
everywhere), then add an explicit FOREIGN KEY constraint referencing
DP_ORGANIZATION(ORG_ID) and ensure the index and any related code use the same
type to avoid join and FK failures.

Comment on lines +281 to +284
}

const orgName = org.IDENTIFIER || 'default';
console.log("💳 Found org name:", orgName);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Check Organization model fields
rg -n "IDENTIFIER|ORG_HANDLE|ORGANIZATION_IDENTIFIER" src/models/organization.js -C2

Repository: wso2/api-developer-portal-core

Length of output: 332


🏁 Script executed:

#!/bin/bash
# Search entire organization.js for IDENTIFIER field
echo "=== Checking entire organization.js for IDENTIFIER ==="
rg -n "IDENTIFIER" src/models/organization.js

echo -e "\n=== Checking billingController.js context around line 283 ==="
sed -n '275,290p' src/controllers/billingController.js

echo -e "\n=== Checking how org object is retrieved in billingController.js ==="
rg -n "org\s*=" src/controllers/billingController.js -B2 -A2 | head -40

Repository: wso2/api-developer-portal-core

Length of output: 1375


🏁 Script executed:

#!/bin/bash
# Check the getOrganization implementation in admin.js
echo "=== Checking getOrganization in admin.js ==="
rg -n "getOrganization" src/dao/admin.js -A10

echo -e "\n=== Check other usages of org object in codebase ==="
rg -n "org\.(ORG_HANDLE|ORGANIZATION_IDENTIFIER|IDENTIFIER)" src/ -C1 | head -50

Repository: wso2/api-developer-portal-core

Length of output: 1800


Change org.IDENTIFIER to org.ORG_HANDLE.

The Organization model does not have an IDENTIFIER field. The correct field for organization identifiers is ORG_HANDLE (used as the URL-friendly identifier), or alternatively ORGANIZATION_IDENTIFIER or ORG_NAME. Since line 285 constructs a URL path with this value (/${orgName}/apis/...), ORG_HANDLE is the appropriate choice.

🤖 Prompt for AI Agents
In src/controllers/billingController.js around lines 281 to 284, the code reads
org.IDENTIFIER which does not exist on the Organization model; replace
org.IDENTIFIER with org.ORG_HANDLE (the URL-friendly identifier) and keep the
same fallback (e.g., 'default') so the constructed path `/${orgName}/apis/...`
uses the correct field; if ORG_HANDLE may be absent in some records, consider
falling back to ORGANIZATION_IDENTIFIER or ORG_NAME in order.

Comment on lines +307 to +325
module.exports = {
createCheckoutSessionForSubscription,
registerStripeCheckoutSession,
cancelSubscription,
getSubscriptionBillingStatus,
createBillingPortal,
createBillingPortalByOrg,
handleStripeWebhook,
handleBillingReturn,
getUsageData,
getPaymentMethods,
removePaymentMethod,
getBillingInfo,
getActiveSubscriptions
, addBillingEngineKeys
, updateBillingEngineKeys
, deleteBillingEngineKeys
, getBillingEngineKeys
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Module exports reference functions defined after this block.

module.exports at lines 307-325 references getUsageData, getPaymentMethods, removePaymentMethod, getBillingInfo, getActiveSubscriptions, addBillingEngineKeys, updateBillingEngineKeys, deleteBillingEngineKeys, and getBillingEngineKeys, but these functions are defined after line 325. Move module.exports to the end of the file.

Proposed fix

Move the module.exports block to the end of the file (after line 424).

🤖 Prompt for AI Agents
In src/controllers/billingController.js around lines 307-325, the module.exports
block is placed before several functions it references (getUsageData,
getPaymentMethods, removePaymentMethod, getBillingInfo, getActiveSubscriptions,
addBillingEngineKeys, updateBillingEngineKeys, deleteBillingEngineKeys,
getBillingEngineKeys); cut this entire module.exports block and paste it at the
end of the file (after line 424) so it exports the functions after they are
defined, preserving the same export order and commas and ensuring the file ends
with the single module.exports statement.

Comment on lines +131 to +174
{{#if @root.apiMetadata.monetizationInfo.enabled}}
{{!-- Monetization enabled - check if plan is paid --}}
{{#if externalPriceId}}
{{!-- Paid plan with Stripe price ID --}}
<a
id="subscribe-btn-{{policyID}}"
type="button"
class="common-btn-primary subscribe-btn {{#if @root.isReadOnlyMode}}disabled{{/if}}"
data-policy-id="{{policyID}}"
data-policy-name="{{policyName}}"
data-external-price-id="{{externalPriceId}}"
data-is-paid="true"
onclick="showSubscribeButtonLoading(this); handleSubscribeWithPayment('{{@root.orgID}}','{{../apiMetadata.apiID}}','{{../apiMetadata.apiReferenceID}}','{{policyID}}','{{policyName}}', this)"
>
Subscribe &amp; Pay
</a>
{{else}}
{{!-- Free plan --}}
<a
id="subscribe-btn-{{policyID}}"
type="button"
class="common-btn-primary subscribe-btn {{#if @root.isReadOnlyMode}}disabled{{/if}}"
data-policy-id="{{policyID}}"
data-policy-name="{{policyName}}"
data-is-paid="false"
onclick="showSubscribeButtonLoading(this); subscribe('{{@root.orgID}}', '', '{{../apiMetadata.apiID}}', '{{../apiMetadata.apiReferenceID}}', '{{policyID}}', '{{policyName}}')"
>
Subscribe
</a>
{{/if}}
{{else}}
{{!-- Monetization disabled - all plans are free --}}
<a
id="subscribe-btn-{{policyID}}"
type="button"
class="common-btn-primary subscribe-btn {{#if @root.isReadOnlyMode}}disabled{{/if}}"
data-policy-id="{{policyID}}"
data-policy-name="{{policyName}}"
data-is-paid="false"
onclick="showSubscribeButtonLoading(this); subscribe('{{@root.orgID}}', '', '{{../apiMetadata.apiID}}', '{{../apiMetadata.apiReferenceID}}', '{{policyID}}', '{{policyName}}')"
>
Subscribe
</a>
{{/if}}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
# Search for externalPriceId usage in JS files to understand how it's exposed
rg -n "externalPriceId" --type js -C 3 | head -100

Repository: wso2/api-developer-portal-core

Length of output: 2880


🏁 Script executed:

#!/bin/bash
# Find where api-subscription-plans.hbs is rendered and what data is passed
rg -n "api-subscription-plans" --type js -C 3

Repository: wso2/api-developer-portal-core

Length of output: 573


🏁 Script executed:

#!/bin/bash
# Find the template file and search for route handlers
find . -name "*.js" -type f | xargs rg -l "api-landing" | head -20

Repository: wso2/api-developer-portal-core

Length of output: 153


🏁 Script executed:

#!/bin/bash
# Examine the apiContentController.js to see how the template is rendered
cat -n src/controllers/apiContentController.js | head -150

Repository: wso2/api-developer-portal-core

Length of output: 8105


🏁 Script executed:

#!/bin/bash
# Continue reading apiContentController.js to find api-landing page rendering
cat -n src/controllers/apiContentController.js | sed -n '150,350p'

Repository: wso2/api-developer-portal-core

Length of output: 12204


🏁 Script executed:

#!/bin/bash
# Continue reading to see how subscription plans are rendered
cat -n src/controllers/apiContentController.js | sed -n '350,450p'

Repository: wso2/api-developer-portal-core

Length of output: 5991


🏁 Script executed:

#!/bin/bash
# Now let's find appendSubscriptionPlanDetails function in util.js
rg -n "appendSubscriptionPlanDetails" --type js -A 30 | head -80

Repository: wso2/api-developer-portal-core

Length of output: 6470


🏁 Script executed:

#!/bin/bash
# Check the subscriptionPolicyDTO to see what properties it exposes
rg -n "class subscriptionPolicyDTO\|function subscriptionPolicyDTO" --type js -A 40

Repository: wso2/api-developer-portal-core

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Find the subscriptionPolicyDTO file
fd "subscriptionPolicy" --type f --extension js

Repository: wso2/api-developer-portal-core

Length of output: 136


🏁 Script executed:

#!/bin/bash
# Read the subscriptionPolicyDTO file
cat -n src/dto/subscriptionPolicy.js

Repository: wso2/api-developer-portal-core

Length of output: 1927


🏁 Script executed:

#!/bin/bash
# Check the buildPricingMetadata function and how pricingMetadata is populated
rg -n "buildPricingMetadata\|PRICING_METADATA" --type js -B 2 -A 20 | head -100

Repository: wso2/api-developer-portal-core

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Check apiMetadata.js to see how pricingMetadata is built
cat -n src/dao/apiMetadata.js | sed -n '440,460p'

Repository: wso2/api-developer-portal-core

Length of output: 852


🏁 Script executed:

#!/bin/bash
# Find where buildPricingMetadata is called
rg -n "buildPricingMetadata" --type js -B 2 -A 5

Repository: wso2/api-developer-portal-core

Length of output: 904


🏁 Script executed:

#!/bin/bash
# Verify if externalPriceId is ever added to the plan objects passed to template
# Check if there's any code that flattens or adds externalPriceId to subscription plans
rg -n "externalPriceId" src/utils/util.js src/controllers/apiContentController.js src/dto/subscriptionPolicy.js

Repository: wso2/api-developer-portal-core

Length of output: 56


🏁 Script executed:

#!/bin/bash
# Check if externalPriceId is accessed anywhere in the template
rg -n "externalPriceId" --type hbs

Repository: wso2/api-developer-portal-core

Length of output: 946


🏁 Script executed:

#!/bin/bash
# Check the structure more carefully - look at what buildPricingMetadata returns
cat -n src/dao/apiMetadata.js | sed -n '444,480p'

Repository: wso2/api-developer-portal-core

Length of output: 1530


Fix incorrect property access for external price ID in subscription plan check.

Line 133 checks {{#if externalPriceId}} directly on the plan object, but externalPriceId is not exposed at the top level. According to buildPricingMetadata in src/dao/apiMetadata.js (lines 448-452), the external price ID is nested under pricingMetadata.external.priceId. Change the condition to {{#if pricingMetadata.external.priceId}} and update the corresponding data attribute on line 141 to use the same nested path. Also check src/defaultContent/pages/apis/partials/api-listing.hbs and src/defaultContent/pages/apis/partials/subscription-plans.hbs, which incorrectly access pricingMetadata.externalPriceId and should use pricingMetadata.external.priceId instead.

🤖 Prompt for AI Agents
In src/defaultContent/pages/api-landing/partials/api-subscription-plans.hbs
around lines 131 to 174, the template checks and uses a non-existent top-level
externalPriceId; change the condition to use the nested property
pricingMetadata.external.priceId and update the data attribute
(data-external-price-id) to read from pricingMetadata.external.priceId as well;
also search and fix similar incorrect accesses in
src/defaultContent/pages/apis/partials/api-listing.hbs and
src/defaultContent/pages/apis/partials/subscription-plans.hbs where
pricingMetadata.externalPriceId should be replaced with
pricingMetadata.external.priceId.

// Get all subscriptions for the user in this organization
let subscriptions = [];
try {
subscriptions = await adminDao.getAllSubscriptions(orgId, userContext.userId);
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

🧩 Analysis chain

🏁 Script executed:

# First, verify the file structure and find the dao/admin.js file
find . -name "admin.js" -o -name "monetizationService.js" | head -20

Repository: wso2/api-developer-portal-core

Length of output: 130


🏁 Script executed:

# Search for the specific method calls mentioned in the review
rg "getAllSubscriptions|getSubscriptionPolicy|getAPIMetadata" src/services/ -n

Repository: wso2/api-developer-portal-core

Length of output: 2441


🏁 Script executed:

# Check the admin.js file to see what methods are exported
rg -n "module.exports|exports\." src/dao/admin.js | head -100

Repository: wso2/api-developer-portal-core

Length of output: 97


🏁 Script executed:

# Read the module.exports section of admin.js to see all exported methods
cat -n src/dao/admin.js | tail -150

Repository: wso2/api-developer-portal-core

Length of output: 5401


🏁 Script executed:

# Also search for the method definitions themselves in admin.js
rg -n "getAllSubscriptions|getSubscriptionPolicy|getAPIMetadata" src/dao/admin.js

Repository: wso2/api-developer-portal-core

Length of output: 179


🏁 Script executed:

# Check if adminDao is imported in monetizationService and how
rg -n "adminDao|require.*admin" src/services/monetizationService.js | head -20

Repository: wso2/api-developer-portal-core

Length of output: 1383


🏁 Script executed:

# Verify if these methods might exist in other dao files
rg "getAllSubscriptions|getSubscriptionPolicy|getAPIMetadata" src/dao/ -l

Repository: wso2/api-developer-portal-core

Length of output: 113


🏁 Script executed:

# Check if these exact methods are defined anywhere in admin.js
cat -n src/dao/admin.js | grep -E "^[[:space:]]*(const|async|function).*(getAllSubscriptions|getSubscriptionPolicy|getAPIMetadata)"

Repository: wso2/api-developer-portal-core

Length of output: 56


🏁 Script executed:

# Check if these methods are in apiMetadata.js dao
rg -n "getAllSubscriptions|getSubscriptionPolicy|getAPIMetadata" src/dao/apiMetadata.js | head -20

Repository: wso2/api-developer-portal-core

Length of output: 552


🏁 Script executed:

# Check what adminDao is being used for - verify its actual source
grep -n "adminDao" src/services/monetizationService.js | grep -E "(require|import)"

Repository: wso2/api-developer-portal-core

Length of output: 118


🏁 Script executed:

# Verify if getAllSubscriptions exists anywhere in the codebase
rg -n "const getAllSubscriptions|function getAllSubscriptions" src/

Repository: wso2/api-developer-portal-core

Length of output: 157


🏁 Script executed:

# Check the exact signatures of the methods in apiMetadata.js to compare
rg -n "const getSubscriptionPolicy|const getAPIMetadata" src/dao/apiMetadata.js -A 1

Repository: wso2/api-developer-portal-core

Length of output: 384


🏁 Script executed:

# Check what methods are actually exported from apiMetadata.js
rg -n "module.exports" src/dao/apiMetadata.js -A 50 | head -80

Repository: wso2/api-developer-portal-core

Length of output: 1419


🏁 Script executed:

# Double-check that adminDao.getAllSubscriptions is not in admin.js by searching for the exact pattern
rg "getAllSubscriptions" src/dao/admin.js

Repository: wso2/api-developer-portal-core

Length of output: 56


Missing adminDao methods will cause runtime errors.

The following methods are called on adminDao but do not exist in admin.js:

  • Line 582: adminDao.getAllSubscriptions (exists only in adminService.js as a service handler, not as a DAO method)
  • Line 636: adminDao.getSubscriptionPolicy (exists in apiMetadata.js, not admin.js)
  • Line 651: adminDao.getAPIMetadata (exists in apiMetadata.js, not admin.js)

The admin.js DAO exports getSubscriptionPolicyById (not getSubscriptionPolicy), and does not export getAPIMetadata or getAllSubscriptions. These calls will fail at runtime. Either these methods need to be added to admin.js, or the code should use the appropriate DAO (apiMetadata.js for policy/metadata methods) or service layer.

Also applies to: 626-626, 636-636, 651-651

🤖 Prompt for AI Agents
In src/services/monetizationService.js around lines 582, 626, 636 and 651, calls
to adminDao.getAllSubscriptions, adminDao.getSubscriptionPolicy and
adminDao.getAPIMetadata reference DAO methods that don't exist; either add those
functions to admin.js or, preferably, call the correct DAO methods (use the DAO
that actually exports them: e.g. replace adminDao.getSubscriptionPolicy with
apiMetadataDao.getSubscriptionPolicy or adminDao.getSubscriptionPolicyById as
appropriate, replace adminDao.getAPIMetadata with apiMetadataDao.getAPIMetadata,
and replace adminDao.getAllSubscriptions with the DAO that provides subscription
listing or implement a new getAllSubscriptions in admin.js), and update the
require/import statements at the top of the file to import apiMetadataDao (or
the correct DAO) and adjust parameter names/return handling to match the actual
DAO method signatures.

Comment on lines +716 to +720
const paymentMethods = await stripeService.listPaymentMethods(customerId);

// Get customer default payment method
const customer = await stripeService.getCustomer(customerId);
const defaultPaymentMethodId = customer.invoice_settings?.default_payment_method;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Missing stripeSecretKey parameter in Stripe service calls.

Lines 716, 719, 741, and 780 call stripeService methods without passing stripeSecretKey. The Stripe service requires per-org keys.

Proposed fix
 async function getPaymentMethods(userContext, orgId) {
   try {
     // ... existing code ...
     
     const customerId = stripeSubscription.BILLING_CUSTOMER_ID;
     
+    const { secretKey: stripeSecretKey } = await getDecryptedStripeKeysForOrg(orgId);
+    
     // Get payment methods from Stripe
-    const paymentMethods = await stripeService.listPaymentMethods(customerId);
+    const paymentMethods = await stripeService.listPaymentMethods(customerId, stripeSecretKey);
     
     // Get customer default payment method
-    const customer = await stripeService.getCustomer(customerId);
+    const customer = await stripeService.getCustomer(customerId, stripeSecretKey);

Apply similar fixes to removePaymentMethod (line 741) and getBillingInfo (line 780).

🤖 Prompt for AI Agents
In src/services/monetizationService.js around lines 716-720, 740-742 and
778-781, Stripe service methods are called without the required per-org
stripeSecretKey; update the calls to pass stripeSecretKey as the first (or
expected) argument when invoking stripeService.listPaymentMethods,
stripeService.getCustomer, stripeService.removePaymentMethod and
stripeService.getBillingInfo (use the stripeSecretKey available in scope or
passed into the function) so each call supplies the org-specific secret key.

Comment on lines +160 to +163

return;
}

Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical syntax error: Stray return statement outside function.

Line 161 contains return; outside of any function body. This appears to be a remnant of incomplete code or a merge issue. Combined with static analysis hints showing "Illegal return statement outside of a function", this file will fail to parse.

Analysis of the issue

The applyWebhookToLocalState function starting at line 142 is missing its closing brace. The early return at line 161 and subsequent code (lines 164-216) should be inside that function. Additionally, module.exports at line 218 appears before several function definitions (lines 236-289), causing those functions to be undefined when exported.

The file structure should be:

  1. Fix the missing } for applyWebhookToLocalState
  2. Move module.exports to the end of the file after all function definitions
🤖 Prompt for AI Agents
In src/services/stripeService.js around lines 142–163 and onward, there's a
stray "return;" at line 161 because the applyWebhookToLocalState function is
missing its closing brace, leaving that return and subsequent code outside any
function and causing parse errors; close the applyWebhookToLocalState function
by adding the missing "}" just before that return so the return (and the rest of
the webhook-handling code) is inside the function, then move the module.exports
statement to the end of the file after all function definitions (lines ~236–289)
so exported functions are defined before export.

Comment on lines +218 to +234
module.exports = {
findOrCreateCustomerByEmail,
createCheckoutSession,
retrieveCheckoutSession,
cancelSubscription,
listInvoicesByCustomer,
listInvoices,
getInvoice,
createCustomerPortalSession,
createPortalSession,
verifyAndConstructWebhookEvent,
applyWebhookToLocalState,
listPaymentMethods,
getCustomer,
detachPaymentMethod,
getSubscription
};
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Module exports reference functions defined after this block.

The exports at lines 218-234 reference listInvoices, listPaymentMethods, getCustomer, detachPaymentMethod, getSubscription, and createPortalSession, but these functions are defined at lines 236-289. Move module.exports to the end of the file.

Proposed fix

Move the entire module.exports block to line 290, after all function definitions.

🤖 Prompt for AI Agents
In src/services/stripeService.js around lines 218-234 the module.exports block
references functions (listInvoices, listPaymentMethods, getCustomer,
detachPaymentMethod, getSubscription, createPortalSession) that are defined
later (lines ~236-289); move the entire module.exports block to the end of the
file (after the last function definition, ~line 290) so all exported identifiers
are defined before export.

@@ -0,0 +1,23 @@
const crypto = require('crypto');

const ENCRYPTION_KEY = process.env.BILLING_KEY_ENCRYPTION_KEY || 'changemechangemechangemechangeme'; // 32 chars for AES-256
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

⚠️ Potential issue | 🔴 Critical

Critical: Hardcoded fallback encryption key is a security vulnerability.

Using a hardcoded default key allows encrypted data to be decrypted by anyone with access to the source code. The application should fail to start if BILLING_KEY_ENCRYPTION_KEY is not set, rather than silently using an insecure default.

🔎 Proposed fix
-const ENCRYPTION_KEY = process.env.BILLING_KEY_ENCRYPTION_KEY || 'changemechangemechangemechangeme'; // 32 chars for AES-256
+const ENCRYPTION_KEY = process.env.BILLING_KEY_ENCRYPTION_KEY;
+
+if (!ENCRYPTION_KEY) {
+    throw new Error('BILLING_KEY_ENCRYPTION_KEY environment variable is required');
+}
+
+if (Buffer.byteLength(ENCRYPTION_KEY) !== 32) {
+    throw new Error('BILLING_KEY_ENCRYPTION_KEY must be exactly 32 bytes for AES-256');
+}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
const ENCRYPTION_KEY = process.env.BILLING_KEY_ENCRYPTION_KEY || 'changemechangemechangemechangeme'; // 32 chars for AES-256
const ENCRYPTION_KEY = process.env.BILLING_KEY_ENCRYPTION_KEY;
if (!ENCRYPTION_KEY) {
throw new Error('BILLING_KEY_ENCRYPTION_KEY environment variable is required');
}
if (Buffer.byteLength(ENCRYPTION_KEY) !== 32) {
throw new Error('BILLING_KEY_ENCRYPTION_KEY must be exactly 32 bytes for AES-256');
}
🤖 Prompt for AI Agents
In src/utils/cryptoUtil.js around line 3, the code currently falls back to a
hardcoded 32-char encryption key when process.env.BILLING_KEY_ENCRYPTION_KEY is
not set, which is a security risk; remove the insecure default and instead check
for the presence of BILLING_KEY_ENCRYPTION_KEY at startup and immediately throw
a clear error (or exit the process) if it's undefined so the application fails
fast and does not run with a predictable key; update the code to read the env
var only and perform a runtime guard that raises an exception with a descriptive
message like "BILLING_KEY_ENCRYPTION_KEY must be set" when missing.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant