diff --git a/apps/bff/src/integrations/salesforce/services/opportunity-resolution.service.ts b/apps/bff/src/integrations/salesforce/services/opportunity-resolution.service.ts
index 780de186..c828f92f 100644
--- a/apps/bff/src/integrations/salesforce/services/opportunity-resolution.service.ts
+++ b/apps/bff/src/integrations/salesforce/services/opportunity-resolution.service.ts
@@ -83,13 +83,14 @@ export class OpportunityResolutionService {
accountId: string | null;
orderType: OrderTypeValue;
existingOpportunityId?: string | undefined;
- }): Promise {
- if (!params.accountId) return null;
+ }): Promise<{ opportunityId: string | null; wasCreated: boolean }> {
+ if (!params.accountId) return { opportunityId: null, wasCreated: false };
const safeAccountId = assertSalesforceId(params.accountId, "accountId");
if (params.existingOpportunityId) {
- return assertSalesforceId(params.existingOpportunityId, "existingOpportunityId");
+ const validated = assertSalesforceId(params.existingOpportunityId, "existingOpportunityId");
+ return { opportunityId: validated, wasCreated: false };
}
const productType = this.mapOrderTypeToProductType(params.orderType);
@@ -105,7 +106,7 @@ export class OpportunityResolutionService {
);
if (existing) {
- return existing;
+ return { opportunityId: existing, wasCreated: false };
}
const created = await this.opportunities.createOpportunity({
@@ -123,7 +124,7 @@ export class OpportunityResolutionService {
orderType: params.orderType,
});
- return created;
+ return { opportunityId: created, wasCreated: true };
},
{ ttlMs: 10_000 }
);
diff --git a/apps/bff/src/modules/orders/services/order-orchestrator.service.ts b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts
index d8a3f6ac..803a76f1 100644
--- a/apps/bff/src/modules/orders/services/order-orchestrator.service.ts
+++ b/apps/bff/src/modules/orders/services/order-orchestrator.service.ts
@@ -3,6 +3,8 @@ import { Logger } from "nestjs-pino";
import { SalesforceOrderService } from "@bff/integrations/salesforce/services/salesforce-order.service.js";
import { SalesforceOpportunityService } from "@bff/integrations/salesforce/services/salesforce-opportunity.service.js";
import { OpportunityResolutionService } from "@bff/integrations/salesforce/services/opportunity-resolution.service.js";
+import { SalesforceCaseService } from "@bff/integrations/salesforce/services/salesforce-case.service.js";
+import { SalesforceConnection } from "@bff/integrations/salesforce/services/salesforce-connection.service.js";
import { OrderValidator } from "./order-validator.service.js";
import { OrderBuilder } from "./order-builder.service.js";
import { OrderItemBuilder } from "./order-item-builder.service.js";
@@ -16,6 +18,8 @@ import type {
} from "@customer-portal/domain/orders";
import { assertSalesforceId } from "@bff/integrations/salesforce/utils/soql.util.js";
import { OPPORTUNITY_STAGE } from "@customer-portal/domain/opportunity";
+import { SALESFORCE_CASE_ORIGIN } from "@customer-portal/domain/support/providers";
+import { extractErrorMessage } from "@bff/core/utils/error.util.js";
type OrderDetailsResponse = OrderDetails;
type OrderSummaryResponse = OrderSummary;
@@ -31,6 +35,8 @@ export class OrderOrchestrator {
private readonly salesforceOrderService: SalesforceOrderService,
private readonly opportunityService: SalesforceOpportunityService,
private readonly opportunityResolution: OpportunityResolutionService,
+ private readonly caseService: SalesforceCaseService,
+ private readonly sfConnection: SalesforceConnection,
private readonly orderValidator: OrderValidator,
private readonly orderBuilder: OrderBuilder,
private readonly orderItemBuilder: OrderItemBuilder,
@@ -57,7 +63,7 @@ export class OrderOrchestrator {
);
// 2) Resolve Opportunity for this order
- const opportunityId = await this.resolveOpportunityForOrder(
+ const { opportunityId, wasCreated: opportunityCreated } = await this.resolveOpportunityForOrder(
validatedBody.orderType,
userMapping.sfAccountId ?? null,
validatedBody.opportunityId
@@ -113,6 +119,17 @@ export class OrderOrchestrator {
}
}
+ // 5) Create internal "Order Placed" case for CS team
+ if (userMapping.sfAccountId) {
+ await this.createOrderPlacedCase({
+ accountId: userMapping.sfAccountId,
+ orderId: created.id,
+ orderType: validatedBody.orderType,
+ opportunityId,
+ opportunityCreated,
+ });
+ }
+
if (userMapping.sfAccountId) {
await this.ordersCache.invalidateAccountOrders(userMapping.sfAccountId);
}
@@ -124,6 +141,7 @@ export class OrderOrchestrator {
skuCount: validatedBody.skus.length,
orderItemCount: orderItemsPayload.length,
opportunityId,
+ opportunityCreated,
},
"Order creation workflow completed successfully"
);
@@ -135,6 +153,66 @@ export class OrderOrchestrator {
};
}
+ /**
+ * Create an internal case when an order is placed.
+ * This is for CS team visibility - not visible to customers.
+ */
+ private async createOrderPlacedCase(params: {
+ accountId: string;
+ orderId: string;
+ orderType: OrderTypeValue;
+ opportunityId: string | null;
+ opportunityCreated: boolean;
+ }): Promise {
+ try {
+ const instanceUrl = this.sfConnection.getInstanceUrl();
+ const orderLink = instanceUrl
+ ? `${instanceUrl}/lightning/r/Order/${params.orderId}/view`
+ : null;
+ const opportunityLink =
+ params.opportunityId && instanceUrl
+ ? `${instanceUrl}/lightning/r/Opportunity/${params.opportunityId}/view`
+ : null;
+
+ const opportunityStatus = params.opportunityId
+ ? params.opportunityCreated
+ ? "Created new opportunity for this order"
+ : "Linked to existing opportunity"
+ : "No opportunity linked";
+
+ const descriptionLines = [
+ "Order placed via Customer Portal.",
+ "",
+ `Order ID: ${params.orderId}`,
+ orderLink ? `Order: ${orderLink}` : null,
+ "",
+ params.opportunityId ? `Opportunity ID: ${params.opportunityId}` : null,
+ opportunityLink ? `Opportunity: ${opportunityLink}` : null,
+ `Opportunity Status: ${opportunityStatus}`,
+ ].filter(Boolean);
+
+ await this.caseService.createCase({
+ accountId: params.accountId,
+ opportunityId: params.opportunityId ?? undefined,
+ subject: `Order Placed - ${params.orderType} (Portal)`,
+ description: descriptionLines.join("\n"),
+ origin: SALESFORCE_CASE_ORIGIN.PORTAL_NOTIFICATION,
+ });
+
+ this.logger.log("Created Order Placed case", {
+ orderId: params.orderId,
+ opportunityIdTail: params.opportunityId?.slice(-4),
+ opportunityCreated: params.opportunityCreated,
+ });
+ } catch (error) {
+ // Log but don't fail the order
+ this.logger.warn("Failed to create Order Placed case", {
+ orderId: params.orderId,
+ error: extractErrorMessage(error),
+ });
+ }
+ }
+
/**
* Resolve Opportunity for an order
*
@@ -146,26 +224,27 @@ export class OrderOrchestrator {
orderType: OrderTypeValue,
sfAccountId: string | null,
existingOpportunityId?: string
- ): Promise {
+ ): Promise<{ opportunityId: string | null; wasCreated: boolean }> {
try {
- const resolved = await this.opportunityResolution.resolveForOrderPlacement({
+ const result = await this.opportunityResolution.resolveForOrderPlacement({
accountId: sfAccountId,
orderType,
existingOpportunityId,
});
- if (resolved) {
+ if (result.opportunityId) {
this.logger.debug("Resolved Opportunity for order", {
- opportunityIdTail: resolved.slice(-4),
+ opportunityIdTail: result.opportunityId.slice(-4),
+ wasCreated: result.wasCreated,
orderType,
});
}
- return resolved;
+ return result;
} catch {
const accountIdTail =
typeof sfAccountId === "string" && sfAccountId.length >= 4 ? sfAccountId.slice(-4) : "none";
this.logger.warn("Failed to resolve Opportunity for order", { orderType, accountIdTail });
// Don't fail the order if Opportunity resolution fails
- return null;
+ return { opportunityId: null, wasCreated: false };
}
}
diff --git a/apps/bff/src/modules/services/application/internet-eligibility.service.ts b/apps/bff/src/modules/services/application/internet-eligibility.service.ts
index cb4211eb..802a1438 100644
--- a/apps/bff/src/modules/services/application/internet-eligibility.service.ts
+++ b/apps/bff/src/modules/services/application/internet-eligibility.service.ts
@@ -125,11 +125,17 @@ export class InternetEligibilityService {
? `${instanceUrl}/lightning/r/Opportunity/${opportunityId}/view`
: null;
+ const opportunityStatus = opportunityCreated
+ ? "Created new opportunity for this request"
+ : "Linked to existing opportunity";
+
const descriptionLines: string[] = [
"Customer requested to check if internet service is available at the following address:",
"",
request.address ? formatAddressForLog(request.address) : "",
- opportunityLink ? `\n\nOpportunity: ${opportunityLink}` : "",
+ "",
+ opportunityLink ? `Opportunity: ${opportunityLink}` : "",
+ `Opportunity Status: ${opportunityStatus}`,
].filter(Boolean);
// 3) Create Case linked to Opportunity (internal workflow case)
diff --git a/apps/portal/src/components/atoms/button.tsx b/apps/portal/src/components/atoms/button.tsx
index bbba8d07..99669e04 100644
--- a/apps/portal/src/components/atoms/button.tsx
+++ b/apps/portal/src/components/atoms/button.tsx
@@ -87,7 +87,7 @@ const Button = forwardRef((p
>
{loading ? : leftIcon}
- {loading ? (loadingText ?? children) : children}
+ {loading ? (loadingText ?? children) : children}
{!loading && rightIcon ? (
{rightIcon}
@@ -108,7 +108,7 @@ const Button = forwardRef((p
>
{loading ? : leftIcon}
- {loading ? (loadingText ?? children) : children}
+ {loading ? (loadingText ?? children) : children}
{!loading && rightIcon ? (
{rightIcon}
@@ -138,7 +138,7 @@ const Button = forwardRef((p
>
{loading ? : leftIcon}
- {loading ? (loadingText ?? children) : children}
+ {loading ? (loadingText ?? children) : children}
{!loading && rightIcon ? (
{rightIcon}
diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx
index fcd43663..794db36c 100644
--- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx
+++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/AccountStatusStep.tsx
@@ -17,7 +17,6 @@ import {
UserCircleIcon,
ArrowRightIcon,
DocumentCheckIcon,
- UserPlusIcon,
} from "@heroicons/react/24/outline";
import { CheckCircle2 } from "lucide-react";
import { useGetStartedStore } from "../../../stores/get-started.store";
@@ -170,17 +169,9 @@ export function AccountStatusStep() {
-
-
- Want to check service availability first?{" "}
-
- Check eligibility
- {" "}
- without creating an account.
-
);
}
diff --git a/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx b/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx
index 39f27458..70c9d553 100644
--- a/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx
+++ b/apps/portal/src/features/get-started/components/GetStartedForm/steps/CompleteAccountStep.tsx
@@ -181,17 +181,48 @@ export function CompleteAccountStep() {
- {/* Pre-filled info display (SF-only users) */}
+ {/* Pre-filled info display in gray disabled inputs (SF-only users) */}
{hasPrefill && (
-
-
Account for:
-
- {prefill?.firstName} {prefill?.lastName}
-
+
+
+
+
+
+
{prefill?.address && (
-
- {prefill.address.city}, {prefill.address.state}
-
+
+
+
+
)}
)}
@@ -252,53 +283,6 @@ export function CompleteAccountStep() {
>
)}
- {/* Password */}
-
-
-
{
- setPassword(e.target.value);
- setLocalErrors(prev => ({ ...prev, password: undefined }));
- }}
- placeholder="Create a strong password"
- disabled={loading}
- error={localErrors.password}
- autoComplete="new-password"
- />
- {localErrors.password &&
{localErrors.password}
}
-
- At least 8 characters with uppercase, lowercase, and numbers
-
-
-
- {/* Confirm Password */}
-
-
-
{
- setConfirmPassword(e.target.value);
- setLocalErrors(prev => ({ ...prev, confirmPassword: undefined }));
- }}
- placeholder="Confirm your password"
- disabled={loading}
- error={localErrors.confirmPassword}
- autoComplete="new-password"
- />
- {localErrors.confirmPassword && (
-
{localErrors.confirmPassword}
- )}
-
-
{/* Phone */}
+ {/* Password (at bottom before terms) */}
+
+
+
{
+ setPassword(e.target.value);
+ setLocalErrors(prev => ({ ...prev, password: undefined }));
+ }}
+ placeholder="Create a strong password"
+ disabled={loading}
+ error={localErrors.password}
+ autoComplete="new-password"
+ />
+ {localErrors.password &&
{localErrors.password}
}
+
+ At least 8 characters with uppercase, lowercase, and numbers
+
+
+
+ {/* Confirm Password */}
+
+
+
{
+ setConfirmPassword(e.target.value);
+ setLocalErrors(prev => ({ ...prev, confirmPassword: undefined }));
+ }}
+ placeholder="Confirm your password"
+ disabled={loading}
+ error={localErrors.confirmPassword}
+ autoComplete="new-password"
+ />
+ {localErrors.confirmPassword && (
+
{localErrors.confirmPassword}
+ )}
+
+
{/* Terms & Marketing */}
diff --git a/apps/portal/src/features/get-started/components/InlineGetStartedSection/InlineGetStartedSection.tsx b/apps/portal/src/features/get-started/components/InlineGetStartedSection/InlineGetStartedSection.tsx
new file mode 100644
index 00000000..e6e7ec60
--- /dev/null
+++ b/apps/portal/src/features/get-started/components/InlineGetStartedSection/InlineGetStartedSection.tsx
@@ -0,0 +1,196 @@
+/**
+ * InlineGetStartedSection - Inline email-first registration for service pages
+ *
+ * Uses the get-started store flow (email → OTP → status → form) inline on service pages
+ * like the SIM configure page. Supports service context to track plan selection through the flow.
+ */
+
+"use client";
+
+import { useState, useEffect } from "react";
+import { useRouter } from "next/navigation";
+import { Button } from "@/components/atoms";
+import { LoginForm } from "@/features/auth/components/LoginForm/LoginForm";
+import { LinkWhmcsForm } from "@/features/auth/components/LinkWhmcsForm/LinkWhmcsForm";
+import { getSafeRedirect } from "@/features/auth/utils/route-protection";
+import { useGetStartedStore, type ServiceContext } from "../../stores/get-started.store";
+import { EmailStep } from "../GetStartedForm/steps/EmailStep";
+import { VerificationStep } from "../GetStartedForm/steps/VerificationStep";
+import { AccountStatusStep } from "../GetStartedForm/steps/AccountStatusStep";
+import { CompleteAccountStep } from "../GetStartedForm/steps/CompleteAccountStep";
+
+interface HighlightItem {
+ title: string;
+ description: string;
+}
+
+interface InlineGetStartedSectionProps {
+ title: string;
+ description?: string;
+ serviceContext?: Omit
;
+ redirectTo?: string;
+ highlights?: HighlightItem[];
+ className?: string;
+}
+
+export function InlineGetStartedSection({
+ title,
+ description,
+ serviceContext,
+ redirectTo,
+ highlights = [],
+ className = "",
+}: InlineGetStartedSectionProps) {
+ const router = useRouter();
+ const [mode, setMode] = useState<"signup" | "login" | "migrate">("signup");
+ const safeRedirect = getSafeRedirect(redirectTo, "/account");
+
+ const { step, reset, setServiceContext } = useGetStartedStore();
+
+ // Set service context when component mounts
+ useEffect(() => {
+ if (serviceContext) {
+ setServiceContext({
+ ...serviceContext,
+ redirectTo: safeRedirect,
+ });
+ }
+ return () => {
+ // Clear service context when unmounting
+ setServiceContext(null);
+ };
+ }, [serviceContext, safeRedirect, setServiceContext]);
+
+ // Reset get-started store when switching to signup mode
+ const handleModeChange = (newMode: "signup" | "login" | "migrate") => {
+ if (newMode === "signup" && mode !== "signup") {
+ reset();
+ // Re-set service context after reset
+ if (serviceContext) {
+ setServiceContext({
+ ...serviceContext,
+ redirectTo: safeRedirect,
+ });
+ }
+ }
+ setMode(newMode);
+ };
+
+ // Render the current step for signup flow
+ const renderSignupStep = () => {
+ switch (step) {
+ case "email":
+ return ;
+ case "verification":
+ return ;
+ case "account-status":
+ return ;
+ case "complete-account":
+ return ;
+ case "success":
+ // Redirect on success
+ router.push(safeRedirect);
+ return null;
+ default:
+ return ;
+ }
+ };
+
+ return (
+
+
+
{title}
+ {description && (
+
{description}
+ )}
+
+
+
+
+
+
+
+
+
+
+
+
+ {mode === "signup" && (
+ <>
+
Create your account
+
+ Verify your email to get started.
+
+ {renderSignupStep()}
+ >
+ )}
+ {mode === "login" && (
+ <>
+
Sign in
+
Access your account to continue.
+
+ >
+ )}
+ {mode === "migrate" && (
+ <>
+
Migrate your account
+
+ Use your legacy portal credentials to transfer your account.
+
+
{
+ if (result.needsPasswordSet) {
+ const params = new URLSearchParams({
+ email: result.user.email,
+ redirect: safeRedirect,
+ });
+ router.push(`/auth/set-password?${params.toString()}`);
+ return;
+ }
+ router.push(safeRedirect);
+ }}
+ />
+ >
+ )}
+
+
+
+ {highlights.length > 0 && (
+
+
+ {highlights.map(item => (
+
+
{item.title}
+
{item.description}
+
+ ))}
+
+
+ )}
+
+ );
+}
diff --git a/apps/portal/src/features/get-started/components/InlineGetStartedSection/index.ts b/apps/portal/src/features/get-started/components/InlineGetStartedSection/index.ts
new file mode 100644
index 00000000..df191d79
--- /dev/null
+++ b/apps/portal/src/features/get-started/components/InlineGetStartedSection/index.ts
@@ -0,0 +1 @@
+export { InlineGetStartedSection } from "./InlineGetStartedSection";
diff --git a/apps/portal/src/features/get-started/components/index.ts b/apps/portal/src/features/get-started/components/index.ts
index 37311086..e42486de 100644
--- a/apps/portal/src/features/get-started/components/index.ts
+++ b/apps/portal/src/features/get-started/components/index.ts
@@ -1,2 +1,3 @@
export { GetStartedForm } from "./GetStartedForm";
export { OtpInput } from "./OtpInput";
+export { InlineGetStartedSection } from "./InlineGetStartedSection";
diff --git a/apps/portal/src/features/get-started/index.ts b/apps/portal/src/features/get-started/index.ts
index 8220e616..ac606d44 100644
--- a/apps/portal/src/features/get-started/index.ts
+++ b/apps/portal/src/features/get-started/index.ts
@@ -12,13 +12,14 @@
export { GetStartedView } from "./views";
// Components
-export { GetStartedForm, OtpInput } from "./components";
+export { GetStartedForm, OtpInput, InlineGetStartedSection } from "./components";
// Store
export {
useGetStartedStore,
type GetStartedStep,
type GetStartedState,
+ type ServiceContext,
} from "./stores/get-started.store";
// API
diff --git a/apps/portal/src/features/get-started/stores/get-started.store.ts b/apps/portal/src/features/get-started/stores/get-started.store.ts
index c3ed26f4..1d3ce162 100644
--- a/apps/portal/src/features/get-started/stores/get-started.store.ts
+++ b/apps/portal/src/features/get-started/stores/get-started.store.ts
@@ -19,6 +19,16 @@ export type GetStartedStep =
| "complete-account"
| "success";
+/**
+ * Service context for tracking which service flow the user came from
+ * (e.g., SIM plan selection)
+ */
+export interface ServiceContext {
+ type: "sim" | "internet" | null;
+ planSku?: string | undefined;
+ redirectTo?: string | undefined;
+}
+
/**
* Address data format used in the get-started form
*/
@@ -66,6 +76,9 @@ export interface GetStartedState {
// Handoff token from eligibility check (for pre-filling data after OTP verification)
handoffToken: string | null;
+ // Service context for tracking which service flow the user came from
+ serviceContext: ServiceContext | null;
+
// Loading and error states
loading: boolean;
error: string | null;
@@ -91,6 +104,7 @@ export interface GetStartedState {
setPrefill: (prefill: VerifyCodeResponse["prefill"]) => void;
setSessionToken: (token: string | null) => void;
setHandoffToken: (token: string | null) => void;
+ setServiceContext: (context: ServiceContext | null) => void;
// Reset
reset: () => void;
@@ -119,6 +133,7 @@ const initialState = {
formData: initialFormData,
prefill: null,
handoffToken: null,
+ serviceContext: null as ServiceContext | null,
loading: false,
error: null,
codeSent: false,
@@ -285,6 +300,10 @@ export const useGetStartedStore = create()((set, get) => ({
set({ handoffToken: token });
},
+ setServiceContext: (context: ServiceContext | null) => {
+ set({ serviceContext: context });
+ },
+
reset: () => {
set(initialState);
},
diff --git a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
index 2c2396c9..c4359110 100644
--- a/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
+++ b/apps/portal/src/features/landing-page/views/PublicLandingView.tsx
@@ -67,7 +67,7 @@ export function PublicLandingView() {
style={{ animationDelay: "300ms" }}
>
Browse Services
diff --git a/apps/portal/src/features/services/components/eligibility-check/steps/SuccessStep.tsx b/apps/portal/src/features/services/components/eligibility-check/steps/SuccessStep.tsx
index ee7f9703..22e3bbb2 100644
--- a/apps/portal/src/features/services/components/eligibility-check/steps/SuccessStep.tsx
+++ b/apps/portal/src/features/services/components/eligibility-check/steps/SuccessStep.tsx
@@ -118,9 +118,6 @@ export function SuccessStep() {
>
View Internet Plans
-
- Create an account to track your request and order services faster.
-
)}
diff --git a/apps/portal/src/features/services/views/PublicSimConfigure.tsx b/apps/portal/src/features/services/views/PublicSimConfigure.tsx
index 681f6090..d6b20f7b 100644
--- a/apps/portal/src/features/services/views/PublicSimConfigure.tsx
+++ b/apps/portal/src/features/services/views/PublicSimConfigure.tsx
@@ -6,7 +6,7 @@ import { AlertBanner } from "@/components/molecules/AlertBanner/AlertBanner";
import { ServicesBackLink } from "@/features/services/components/base/ServicesBackLink";
import { useServicesBasePath } from "@/features/services/hooks/useServicesBasePath";
import { usePublicSimPlan } from "@/features/services/hooks";
-import { InlineAuthSection } from "@/features/auth/components/InlineAuthSection/InlineAuthSection";
+import { InlineGetStartedSection } from "@/features/get-started";
import { CardPricing } from "@/features/services/components/base/CardPricing";
import { Skeleton } from "@/components/atoms/loading-skeleton";
@@ -159,9 +159,10 @@ export function PublicSimConfigureView() {
{/* Auth Section */}
-