Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
31 changes: 31 additions & 0 deletions Core/Resgrid.Config/BusinessOperationsAddonConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
namespace Resgrid.Config
{
/// <summary>
/// Department-level monthly Business Operations add-on prices and provider product ids (Workforce &amp; Business
/// Operations plan, decision 42; PlanAddonTypes.BusinessOperations = 4). Provider checkout lives in the Billing API
/// (int-CommonApis), cloned from Readiness Pro; Core only proxies it. Environment keys:
/// RESGRID:BusinessOperationsAddonConfig:StripeProductId, :StripeTestProductId, :StripePriceId, :StripeTestPriceId,
/// :PaddleProductId, :PaddleTestProductId, :StripeMonthlyAmount, :PaddleMonthlyAmount. The Paddle price id lives in
/// PaymentProviderConfig.PaddleBusinessOperationsAddon (the Readiness Pro convention). Live ids set 2026-09-18:
/// Stripe product prod_VHnlBsvKsSpqeP / price price_0UHEA6qJFDZJcnkVnj0ZaAFw (USD 250/month), Paddle product

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

kody code-review Kody Rules high

Sensitive identifier exposure in Core/Resgrid.Config/BusinessOperationsAddonConfig.cs and Core/Resgrid.Services/Invoicing/InvoicingService.cs:875-884: source comments embed live provider identifiers such as prod_VHnlBsvKsSpqeP and price_0UHEA6qJFDZJcnkVnj0ZaAFw. Remove concrete IDs from comments and reference secured configuration or audit records instead.

Kody rule violation: Emit tamper-evident audit logs with required fields

Prompt for LLM

File Core/Resgrid.Config/BusinessOperationsAddonConfig.cs:

Line 10:

Sensitive identifier exposure in `Core/Resgrid.Config/BusinessOperationsAddonConfig.cs` and `Core/Resgrid.Services/Invoicing/InvoicingService.cs:875-884`: source comments embed live provider identifiers such as `prod_VHnlBsvKsSpqeP` and `price_0UHEA6qJFDZJcnkVnj0ZaAFw`. Remove concrete IDs from comments and reference secured configuration or audit records instead.

Talk to Kody by mentioning @kody

Was this suggestion helpful? React with 👍 or 👎 to help Kody learn from this interaction.

/// pro_01m2vrcv37k2pnqseb22d8r244 / price pri_01m2vrdycmx8kfhys5xxcjgqnx (EUR 295/month); test-mode ids are still empty.
/// </summary>
public static class BusinessOperationsAddonConfig
{
/// <summary>Fixed PlanAddons row id so every data center's catalog row matches (M0211; the PTT/ADP/Readiness Pro convention).</summary>
public const string PlanAddonId = "8c2f0d6e-5b1a-4f2e-9d3c-7a6b5e4d3c2b";

public static string StripeProductId = "prod_VHnlBsvKsSpqeP";
public static string StripeTestProductId = "";
public static string StripePriceId = "price_0UHEA6qJFDZJcnkVnj0ZaAFw";
public static string StripeTestPriceId = "";
public static string PaddleProductId = "pro_01m2vrcv37k2pnqseb22d8r244";
public static string PaddleTestProductId = "";

/// <summary>USD per month through Stripe (US cluster). Set 2026-09-18.</summary>
public static decimal StripeMonthlyAmount = 250m;

/// <summary>EUR per month through Paddle (EU cluster). Set 2026-09-18.</summary>
public static decimal PaddleMonthlyAmount = 295m;
}
}
69 changes: 69 additions & 0 deletions Core/Resgrid.Config/PaymentConnectConfig.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,69 @@
namespace Resgrid.Config
{
/// <summary>
/// Department-connected Stripe accounts for collecting payments on Resgrid invoices (Workforce &amp; Business
/// Operations plan, Phase B2). Off unless this switch is on; the operator feature flag Payments.StripeConnect is
/// the second, per-cluster lock, so a cluster that does not offer payment collection leaves both off. These are
/// the platform's own Connect credentials and are deliberately separate from PaymentProviderConfig, which serves
/// Resgrid's SaaS subscription billing even when both point at the same Stripe account. Environment keys:
/// RESGRID:PaymentConnectConfig:Enabled, :CredentialPassphrase, :PublicBaseUrl, :StripeClientId, :StripeSecretKey,
/// :StripeConnectWebhookSecret, :StripeLiveMode, :PayLinkTokenTtlDays, :RequestReconcileAfterMinutes,
/// :EventRetentionDays, :PayPageRateLimitPerMinute, :WebhookStaleAfterHours, :WebhookEndpointProbeEnabled.
/// </summary>
public static class PaymentConnectConfig
{
/// <summary>Process-level master switch. Off means no provider is registered, no connection can be made and the webhook endpoint answers 503.</summary>
public static bool Enabled = false;

/// <summary>
/// Passphrase for the symmetric encryption of stored provider tokens. Stripe Connect stores no per-account token,
/// so it is unused in v1, but a later token-holding provider (Square, PayPal, Authorize.net) cannot be enabled without it.
/// </summary>
public static string CredentialPassphrase = "";

/// <summary>Public origin of this cluster (for example https://api.resgrid.com). The pay page, the OAuth redirect and the webhook URL are built from this and nothing else.</summary>
public static string PublicBaseUrl = "";

/// <summary>Connect OAuth client id of the platform (ca_...). Live and sandbox ids differ.</summary>
public static string StripeClientId = "";

/// <summary>The platform's secret key used for the OAuth token exchange and for every call made as a connected account.</summary>
public static string StripeSecretKey = "";

/// <summary>Signing secret of the Connect-scoped webhook endpoint (events from connected accounts). Never the SaaS endpoint's secret.</summary>
public static string StripeConnectWebhookSecret = "";

/// <summary>Whether the keys above are live-mode keys. A webhook whose livemode does not match is rejected.</summary>
public static bool StripeLiveMode = false;

/// <summary>How long an e-mailed or printed pay-page link stays valid.</summary>
public static int PayLinkTokenTtlDays = 30;

/// <summary>Age after which an open payment request is polled at Stripe by the invoice maintenance worker instead of waiting for a webhook.</summary>
public static int RequestReconcileAfterMinutes = 10;

/// <summary>Days raw webhook bodies are kept for forensics before the worker purges them.</summary>
public static int EventRetentionDays = 90;

/// <summary>Per-IP request limit on the anonymous pay page.</summary>
public static int PayPageRateLimitPerMinute = 20;

/// <summary>Hours without any webhook event, while there was payment activity in the last seven days, before the health check reports the webhook as stale.</summary>
public static int WebhookStaleAfterHours = 24;

/// <summary>Whether the health check may ask Stripe whether an enabled webhook endpoint exists at this cluster's webhook URL (one call per process per fifteen minutes).</summary>
public static bool WebhookEndpointProbeEnabled = true;

/// <summary>Route of the Connect webhook receiver on the Web.Services host. A constant, so the config processor never overwrites it.</summary>
public const string WebhookPath = "/api/PaymentWebhooks/stripe";

/// <summary>The webhook URL Stripe must be configured with for this cluster, or an empty string when PublicBaseUrl is not set.</summary>
public static string GetWebhookUrl()
{
if (string.IsNullOrWhiteSpace(PublicBaseUrl))
return string.Empty;

return PublicBaseUrl.TrimEnd('/') + WebhookPath;
}
}
}
12 changes: 11 additions & 1 deletion Core/Resgrid.Config/PaymentProviderConfig.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
using System;
using System;
using System.Text.RegularExpressions;

namespace Resgrid.Config
Expand Down Expand Up @@ -48,6 +48,11 @@ public static class PaymentProviderConfig
// configured separately; a missing sandbox price must never fall back to production.
public static string PaddleReadinessProAddon = "pri_01m20xy5x54j0sp4mcydcm4q6m";
public static string PaddleReadinessProAddonTest = "";

// Business Operations: EUR 295/month, Paddle product pro_01m2vrcv37k2pnqseb22d8r244 (plan decision 42). Stripe
// USD 250/month (price_0UHEA6qJFDZJcnkVnj0ZaAFw) is seeded on PlanAddons by M0211; the test-mode ids are still empty.
public static string PaddleBusinessOperationsAddon = "pri_01m2vrdycmx8kfhys5xxcjgqnx";
public static string PaddleBusinessOperationsAddonTest = "";
public static string PaddleProductionEnvironment = "production";
public static string PaddleTestEnvironment = "sandbox";
public static string PaddleProductionClientToken = "";
Expand Down Expand Up @@ -164,6 +169,11 @@ public static string GetPaddleReadinessProAddonPriceId()
return NormalizeConfigValue(IsTestMode ? PaddleReadinessProAddonTest : PaddleReadinessProAddon);
}

public static string GetPaddleBusinessOperationsAddonPriceId()
{
return NormalizeConfigValue(IsTestMode ? PaddleBusinessOperationsAddonTest : PaddleBusinessOperationsAddon);
}

public static string GetPaddleEnvironment()
{
if (IsTestMode)
Expand Down
Loading
Loading