Add OpenPayd marketplace admin integration
Some checks failed
API CI / API Lint (push) Successful in 38s
API CI / API Type Check (push) Failing after 43s
API CI / API Test (push) Successful in 56s
API CI / API Build (push) Failing after 45s
API CI / Build Docker Image (push) Has been skipped
CD Pipeline / Deploy to Staging (push) Failing after 23s
CI Pipeline / Lint and Type Check (push) Failing after 33s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 1m21s
CI Pipeline / Test Frontend (push) Failing after 32s
CI Pipeline / Security Scan (push) Failing after 1m19s
Deploy to Staging / Deploy to Staging (push) Failing after 27s
Portal CI / Portal Lint (push) Failing after 23s
Portal CI / Portal Type Check (push) Failing after 20s
Portal CI / Portal Test (push) Failing after 23s
Portal CI / Portal Build (push) Failing after 23s
Test Suite / frontend-tests (push) Failing after 31s
Test Suite / api-tests (push) Failing after 45s
Test Suite / blockchain-tests (push) Failing after 31s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 19s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 19s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 21s
CD Pipeline / Deploy to Production (push) Has been skipped
Some checks failed
API CI / API Lint (push) Successful in 38s
API CI / API Type Check (push) Failing after 43s
API CI / API Test (push) Successful in 56s
API CI / API Build (push) Failing after 45s
API CI / Build Docker Image (push) Has been skipped
CD Pipeline / Deploy to Staging (push) Failing after 23s
CI Pipeline / Lint and Type Check (push) Failing after 33s
CI Pipeline / Build (push) Has been skipped
CI Pipeline / Test Backend (push) Failing after 1m21s
CI Pipeline / Test Frontend (push) Failing after 32s
CI Pipeline / Security Scan (push) Failing after 1m19s
Deploy to Staging / Deploy to Staging (push) Failing after 27s
Portal CI / Portal Lint (push) Failing after 23s
Portal CI / Portal Type Check (push) Failing after 20s
Portal CI / Portal Test (push) Failing after 23s
Portal CI / Portal Build (push) Failing after 23s
Test Suite / frontend-tests (push) Failing after 31s
Test Suite / api-tests (push) Failing after 45s
Test Suite / blockchain-tests (push) Failing after 31s
Type Check / type-check (map[directory:. name:root]) (push) Failing after 19s
Type Check / type-check (map[directory:api name:api]) (push) Failing after 19s
Type Check / type-check (map[directory:portal name:portal]) (push) Failing after 21s
CD Pipeline / Deploy to Production (push) Has been skipped
This commit is contained in:
149
portal/src/app/admin/openpayd/page.tsx
Normal file
149
portal/src/app/admin/openpayd/page.tsx
Normal file
@@ -0,0 +1,149 @@
|
||||
'use client';
|
||||
|
||||
import { Download, KeyRound, PauseCircle, PlayCircle, RefreshCw, ShieldCheck } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { RoleGate } from '@/components/auth/RoleGate';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
|
||||
type Environment = 'sandbox' | 'production';
|
||||
|
||||
export default function OpenPaydAdminPage() {
|
||||
return (
|
||||
<RoleGate
|
||||
allowedRoles={['admin', 'tenant-admin', 'ADMIN', 'TENANT_ADMIN']}
|
||||
callbackUrl="/admin/openpayd"
|
||||
badge="OpenPayd Admin"
|
||||
title="OpenPayd operator access required"
|
||||
subtitle="Sign in with a tenant-admin or internal-admin account to manage the OpenPayd connector."
|
||||
>
|
||||
<OpenPaydAdminWorkbench />
|
||||
</RoleGate>
|
||||
);
|
||||
}
|
||||
|
||||
function OpenPaydAdminWorkbench() {
|
||||
const [environment, setEnvironment] = useState<Environment>('sandbox');
|
||||
const [status, setStatus] = useState<Record<string, unknown> | null>(null);
|
||||
const [message, setMessage] = useState<string | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/openpayd/status?environment=${environment}`, { cache: 'no-store' });
|
||||
const payload = await res.json();
|
||||
if (!res.ok) throw new Error(payload.error || payload.message || 'Status load failed');
|
||||
setStatus(payload);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'Status load failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
async function command(name: string, payload: Record<string, unknown> = {}) {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
setMessage(null);
|
||||
try {
|
||||
const res = await fetch(`/api/openpayd/commands/${name}`, {
|
||||
method: 'POST',
|
||||
headers: { 'Content-Type': 'application/json' },
|
||||
body: JSON.stringify({ environment, payload }),
|
||||
});
|
||||
const data = await res.json();
|
||||
if (!res.ok) throw new Error(data.status || data.error || data.message || `${name} failed`);
|
||||
setMessage(`${name}: ${data.status || 'ok'}`);
|
||||
await load();
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : `${name} failed`);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [environment]);
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl px-4 py-10 text-white">
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium uppercase tracking-wide text-orange-400">Operator Console</p>
|
||||
<h1 className="mt-1 text-3xl font-bold">OpenPayd Connector Admin</h1>
|
||||
<p className="mt-2 max-w-3xl text-gray-400">
|
||||
Manage environment probes, lifecycle state, credential rotation workflow, evidence exports, and
|
||||
production gates without exposing raw OpenPayd credentials.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant={environment === 'sandbox' ? 'primary' : 'outline'} onClick={() => setEnvironment('sandbox')}>
|
||||
Sandbox
|
||||
</Button>
|
||||
<Button variant={environment === 'production' ? 'primary' : 'outline'} onClick={() => setEnvironment('production')}>
|
||||
Production
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={load} disabled={loading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" /> Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{message ? <div className="mb-4 rounded-md border border-emerald-800 bg-emerald-950/40 p-3 text-sm text-emerald-200">{message}</div> : null}
|
||||
{error ? <div className="mb-4 rounded-md border border-red-800 bg-red-950/40 p-3 text-sm text-red-200">{error}</div> : null}
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<ActionCard icon={PlayCircle} title="Run Probe" text="Token and read-only account probe." action={() => command('probeEnvironment')} disabled={loading} />
|
||||
<ActionCard icon={KeyRound} title="Rotate Credential" text="Record a credential rotation through secret adapter." action={() => command('rotateCredential')} disabled={loading} />
|
||||
<ActionCard icon={ShieldCheck} title="Production Gate" text="Enable production mutating traffic for approved tenants." action={() => command('setProductionMutatingGate', { enabled: true })} disabled={loading || environment !== 'production'} />
|
||||
<ActionCard icon={PauseCircle} title="Suspend Tenant" text="Block new mutating actions and keep read-only reconciliation." action={() => command('suspendTenant')} disabled={loading} />
|
||||
<ActionCard icon={Download} title="Export Evidence" text="Generate redacted compliance evidence pack." action={() => command('exportComplianceEvidencePack')} disabled={loading} />
|
||||
</div>
|
||||
|
||||
<Card className="mt-6 border-gray-800 bg-gray-900">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<ShieldCheck className="h-5 w-5 text-orange-400" /> Connector State
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<pre className="max-h-[36rem] overflow-auto rounded-md bg-black/30 p-3 text-xs text-gray-300">
|
||||
{JSON.stringify(status || {}, null, 2)}
|
||||
</pre>
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function ActionCard({
|
||||
icon: Icon,
|
||||
title,
|
||||
text,
|
||||
action,
|
||||
disabled,
|
||||
}: {
|
||||
icon: typeof PlayCircle;
|
||||
title: string;
|
||||
text: string;
|
||||
action: () => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
return (
|
||||
<Card className="border-gray-800 bg-gray-900">
|
||||
<CardHeader>
|
||||
<Icon className="h-6 w-6 text-orange-400" />
|
||||
<CardTitle className="text-white">{title}</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<p className="mb-4 text-sm text-gray-400">{text}</p>
|
||||
<Button variant="secondary" onClick={action} disabled={disabled}>Execute</Button>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,6 +1,6 @@
|
||||
'use client';
|
||||
|
||||
import { Building2, Users, CreditCard, Shield, ArrowRight } from 'lucide-react';
|
||||
import { Building2, Users, CreditCard, Shield, ArrowRight, Landmark } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
import { useSession } from 'next-auth/react';
|
||||
|
||||
@@ -39,6 +39,13 @@ export default function AdminPortalPage() {
|
||||
href: '/admin/compliance',
|
||||
features: ['Compliance dashboard', 'Audit logs', 'Export reports'],
|
||||
},
|
||||
{
|
||||
title: 'OpenPayd Connector',
|
||||
description: 'Manage OpenPayd credentials, probes, lifecycle gates, evidence packs, and incidents',
|
||||
icon: Landmark,
|
||||
href: '/admin/openpayd',
|
||||
features: ['Sandbox probes', 'Production gates', 'Evidence export'],
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
|
||||
37
portal/src/app/api/openpayd/commands/[command]/route.ts
Normal file
37
portal/src/app/api/openpayd/commands/[command]/route.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
|
||||
import { authOptions } from '@/lib/auth';
|
||||
|
||||
const API_BASE =
|
||||
process.env.PHOENIX_API_INTERNAL_URL ||
|
||||
process.env.NEXT_PUBLIC_PHOENIX_API_URL ||
|
||||
'https://api.sankofa.nexus';
|
||||
|
||||
type SessionClaims = {
|
||||
accessToken?: string;
|
||||
tenantId?: string;
|
||||
} | null;
|
||||
|
||||
export async function POST(request: Request, { params }: { params: { command: string } }) {
|
||||
const session = (await getServerSession(authOptions)) as SessionClaims;
|
||||
if (!session?.accessToken) {
|
||||
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const body = await request.json().catch(() => ({}));
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
'Content-Type': 'application/json',
|
||||
};
|
||||
if (session.tenantId) headers['X-Tenant-Id'] = session.tenantId;
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/v1/openpayd/commands/${params.command}`, {
|
||||
method: 'POST',
|
||||
headers,
|
||||
body: JSON.stringify(body),
|
||||
cache: 'no-store',
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
}
|
||||
35
portal/src/app/api/openpayd/status/route.ts
Normal file
35
portal/src/app/api/openpayd/status/route.ts
Normal file
@@ -0,0 +1,35 @@
|
||||
import { NextResponse } from 'next/server';
|
||||
import { getServerSession } from 'next-auth';
|
||||
|
||||
import { authOptions } from '@/lib/auth';
|
||||
|
||||
const API_BASE =
|
||||
process.env.PHOENIX_API_INTERNAL_URL ||
|
||||
process.env.NEXT_PUBLIC_PHOENIX_API_URL ||
|
||||
'https://api.sankofa.nexus';
|
||||
|
||||
type SessionClaims = {
|
||||
accessToken?: string;
|
||||
tenantId?: string;
|
||||
} | null;
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const session = (await getServerSession(authOptions)) as SessionClaims;
|
||||
if (!session?.accessToken) {
|
||||
return NextResponse.json({ message: 'Unauthorized' }, { status: 401 });
|
||||
}
|
||||
|
||||
const url = new URL(request.url);
|
||||
const environment = url.searchParams.get('environment') || 'sandbox';
|
||||
const headers: Record<string, string> = {
|
||||
Authorization: `Bearer ${session.accessToken}`,
|
||||
};
|
||||
if (session.tenantId) headers['X-Tenant-Id'] = session.tenantId;
|
||||
|
||||
const res = await fetch(`${API_BASE}/api/v1/openpayd/status?environment=${encodeURIComponent(environment)}`, {
|
||||
headers,
|
||||
cache: 'no-store',
|
||||
});
|
||||
const data = await res.json().catch(() => ({}));
|
||||
return NextResponse.json(data, { status: res.status });
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
'use client';
|
||||
|
||||
import { RefreshCw, ShieldAlert } from 'lucide-react';
|
||||
import { useEffect, useState } from 'react';
|
||||
|
||||
import { Card, CardContent, CardHeader, CardTitle } from '@/components/ui/Card';
|
||||
import { Button } from '@/components/ui/Button';
|
||||
|
||||
type ConnectorStatus = {
|
||||
entitlementStatus?: string;
|
||||
connector?: {
|
||||
ok?: boolean;
|
||||
status?: string;
|
||||
data?: {
|
||||
tenantEnvironment?: Record<string, unknown>;
|
||||
recentCommands?: Record<string, unknown>[];
|
||||
recentWebhookEvents?: Record<string, unknown>[];
|
||||
reconciliationExceptions?: Record<string, unknown>[];
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export default function OpenPaydTenantDashboardPage() {
|
||||
const [environment, setEnvironment] = useState<'sandbox' | 'production'>('sandbox');
|
||||
const [data, setData] = useState<ConnectorStatus | null>(null);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [loading, setLoading] = useState(false);
|
||||
|
||||
async function load() {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const res = await fetch(`/api/openpayd/status?environment=${environment}`, { cache: 'no-store' });
|
||||
const payload = await res.json();
|
||||
if (!res.ok) throw new Error(payload.error || payload.message || 'OpenPayd status failed');
|
||||
setData(payload);
|
||||
} catch (e) {
|
||||
setError(e instanceof Error ? e.message : 'OpenPayd status failed');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
void load();
|
||||
}, [environment]);
|
||||
|
||||
const status = data?.connector?.data?.tenantEnvironment || {};
|
||||
const commands = data?.connector?.data?.recentCommands || [];
|
||||
const events = data?.connector?.data?.recentWebhookEvents || [];
|
||||
const exceptions = data?.connector?.data?.reconciliationExceptions || [];
|
||||
|
||||
return (
|
||||
<main className="mx-auto max-w-6xl px-4 py-10 text-white">
|
||||
<div className="mb-8 flex flex-col gap-4 md:flex-row md:items-end md:justify-between">
|
||||
<div>
|
||||
<p className="text-sm font-medium uppercase tracking-wide text-orange-400">OpenPayd Connector</p>
|
||||
<h1 className="mt-1 text-3xl font-bold">Tenant Dashboard</h1>
|
||||
<p className="mt-2 max-w-3xl text-gray-400">
|
||||
Read-only operational view for entitlement, lifecycle, environment health, commands, webhook facts,
|
||||
and reconciliation exceptions. Sensitive account and credential values are not rendered here.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex gap-2">
|
||||
<Button variant={environment === 'sandbox' ? 'primary' : 'outline'} onClick={() => setEnvironment('sandbox')}>
|
||||
Sandbox
|
||||
</Button>
|
||||
<Button variant={environment === 'production' ? 'primary' : 'outline'} onClick={() => setEnvironment('production')}>
|
||||
Production
|
||||
</Button>
|
||||
<Button variant="secondary" onClick={load} disabled={loading}>
|
||||
<RefreshCw className="mr-2 h-4 w-4" /> Refresh
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{error ? (
|
||||
<Card className="mb-6 border-red-800 bg-red-950/40">
|
||||
<CardContent className="pt-6 text-red-200">{error}</CardContent>
|
||||
</Card>
|
||||
) : null}
|
||||
|
||||
<div className="grid gap-4 md:grid-cols-3">
|
||||
<Card className="border-gray-800 bg-gray-900">
|
||||
<CardHeader><CardTitle className="text-white">Entitlement</CardTitle></CardHeader>
|
||||
<CardContent className="text-sm text-gray-300">{data?.entitlementStatus || 'Loading'}</CardContent>
|
||||
</Card>
|
||||
<Card className="border-gray-800 bg-gray-900">
|
||||
<CardHeader><CardTitle className="text-white">Lifecycle</CardTitle></CardHeader>
|
||||
<CardContent className="text-sm text-gray-300">{String(status.state || 'unknown')}</CardContent>
|
||||
</Card>
|
||||
<Card className="border-gray-800 bg-gray-900">
|
||||
<CardHeader><CardTitle className="text-white">Mutating Gate</CardTitle></CardHeader>
|
||||
<CardContent className="text-sm text-gray-300">{String(status.mutatingActionsEnabled || false)}</CardContent>
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
<div className="mt-6 grid gap-6 lg:grid-cols-2">
|
||||
<RecordList title="Recent Commands" rows={commands} />
|
||||
<RecordList title="Webhook Facts" rows={events} />
|
||||
</div>
|
||||
|
||||
<Card className="mt-6 border-gray-800 bg-gray-900">
|
||||
<CardHeader>
|
||||
<CardTitle className="flex items-center gap-2 text-white">
|
||||
<ShieldAlert className="h-5 w-5 text-orange-400" /> Reconciliation Exceptions
|
||||
</CardTitle>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
{exceptions.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No reconciliation exceptions reported.</p>
|
||||
) : (
|
||||
<pre className="overflow-x-auto rounded-md bg-black/30 p-3 text-xs text-gray-300">
|
||||
{JSON.stringify(exceptions, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
</main>
|
||||
);
|
||||
}
|
||||
|
||||
function RecordList({ title, rows }: { title: string; rows: Record<string, unknown>[] }) {
|
||||
return (
|
||||
<Card className="border-gray-800 bg-gray-900">
|
||||
<CardHeader><CardTitle className="text-white">{title}</CardTitle></CardHeader>
|
||||
<CardContent>
|
||||
{rows.length === 0 ? (
|
||||
<p className="text-sm text-gray-400">No records yet.</p>
|
||||
) : (
|
||||
<pre className="max-h-80 overflow-auto rounded-md bg-black/30 p-3 text-xs text-gray-300">
|
||||
{JSON.stringify(rows, null, 2)}
|
||||
</pre>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
84
portal/src/app/marketplace/openpayd/page.tsx
Normal file
84
portal/src/app/marketplace/openpayd/page.tsx
Normal file
@@ -0,0 +1,84 @@
|
||||
import { ArrowRight, Landmark, LockKeyhole, ShieldCheck } from 'lucide-react';
|
||||
import Link from 'next/link';
|
||||
|
||||
import { CorporateFooter } from '@/components/corporate/CorporateFooter';
|
||||
import { CorporateHeader } from '@/components/corporate/CorporateHeader';
|
||||
|
||||
export const metadata = {
|
||||
title: 'OpenPayd Financial Services Connector — Sankofa Marketplace',
|
||||
description: 'Operator-provisioned OpenPayd connector for Phoenix Financial Cloud subscribers.',
|
||||
};
|
||||
|
||||
const skus = [
|
||||
'openpayd-connector-sandbox',
|
||||
'openpayd-connector-production-readonly',
|
||||
'openpayd-connector-production-payouts',
|
||||
'openpayd-connector-digital-assets-gated',
|
||||
];
|
||||
|
||||
export default function OpenPaydMarketplacePage() {
|
||||
return (
|
||||
<div className="flex min-h-screen flex-col bg-sovereign-obsidian text-sovereign-ivory">
|
||||
<CorporateHeader />
|
||||
<main className="mx-auto w-full max-w-6xl flex-1 px-4 py-16 sm:px-6 lg:px-8">
|
||||
<p className="text-xs font-semibold uppercase tracking-wider text-sovereign-gold">
|
||||
Phoenix Financial Cloud
|
||||
</p>
|
||||
<h1 className="mt-2 text-3xl font-bold tracking-tight sm:text-4xl">
|
||||
OpenPayd Financial Services Connector
|
||||
</h1>
|
||||
<p className="mt-4 max-w-3xl text-lg text-sovereign-ivory/70">
|
||||
Operator-provisioned access to OpenPayd-backed accounts, virtual IBANs, beneficiary validation,
|
||||
payouts, Open Banking, and gated digital-asset capabilities through Phoenix controls.
|
||||
</p>
|
||||
|
||||
<div className="mt-10 grid gap-4 md:grid-cols-3">
|
||||
{[
|
||||
{ icon: Landmark, title: 'Financial rails', text: 'Accounts, payment accounts, beneficiaries, payouts, and Open Banking behind tenant policy.' },
|
||||
{ icon: ShieldCheck, title: 'Phoenix gated', text: 'Entitlements, approval gates, audit records, and reconciliation boundaries stay in Phoenix.' },
|
||||
{ icon: LockKeyhole, title: 'Operator provisioned', text: 'Credentials and production mutating traffic are controlled by authorized operators only.' },
|
||||
].map((item) => {
|
||||
const Icon = item.icon;
|
||||
return (
|
||||
<article key={item.title} className="rounded-lg border border-sovereign-bronze/25 bg-sovereign-midnight/40 p-5">
|
||||
<Icon className="h-6 w-6 text-sovereign-gold" />
|
||||
<h2 className="mt-3 text-lg font-semibold">{item.title}</h2>
|
||||
<p className="mt-2 text-sm text-sovereign-ivory/60">{item.text}</p>
|
||||
</article>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<section className="mt-10 rounded-lg border border-sovereign-bronze/25 bg-sovereign-midnight/30 p-6">
|
||||
<h2 className="text-xl font-semibold">Catalog SKUs</h2>
|
||||
<div className="mt-4 grid gap-3 sm:grid-cols-2">
|
||||
{skus.map((sku) => (
|
||||
<div key={sku} className="rounded-md border border-white/10 bg-black/20 p-3 font-mono text-sm text-sovereign-gold">
|
||||
{sku}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<p className="mt-4 text-sm text-sovereign-ivory/65">
|
||||
Production URLs, production credentials, webhook keys, and digital-asset contracts remain gated
|
||||
OpenPayd onboarding inputs. Production mutating actions are disabled by default.
|
||||
</p>
|
||||
<div className="mt-6 flex flex-col gap-3 sm:flex-row">
|
||||
<Link
|
||||
href="/marketplace/entitlements/phoenix-openpayd-connector"
|
||||
className="inline-flex items-center gap-2 rounded-lg bg-sovereign-gold px-5 py-2.5 text-sm font-semibold text-sovereign-obsidian no-underline hover:bg-sovereign-ivory"
|
||||
>
|
||||
Request subscription <ArrowRight className="h-4 w-4" aria-hidden />
|
||||
</Link>
|
||||
<Link
|
||||
href="/marketplace/entitlements/phoenix-openpayd-connector/dashboard"
|
||||
className="inline-flex items-center gap-2 rounded-lg border border-sovereign-bronze/40 px-5 py-2.5 text-sm font-semibold text-sovereign-ivory no-underline hover:border-sovereign-gold"
|
||||
>
|
||||
Open dashboard
|
||||
</Link>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
<CorporateFooter />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -140,13 +140,23 @@ export const productDivisions: ProductDivision[] = [
|
||||
];
|
||||
|
||||
/** Marketplace partner / product stack entries derived from product divisions. */
|
||||
export const partnerStackServices: PartnerStackService[] = productDivisions.map((division) => ({
|
||||
name: division.name,
|
||||
href: division.href,
|
||||
description: division.description,
|
||||
category: division.tagline,
|
||||
external: division.external,
|
||||
}));
|
||||
export const partnerStackServices: PartnerStackService[] = [
|
||||
...productDivisions.map((division) => ({
|
||||
name: division.name,
|
||||
href: division.href,
|
||||
description: division.description,
|
||||
category: division.tagline,
|
||||
external: division.external,
|
||||
})),
|
||||
{
|
||||
name: 'OpenPayd Financial Services Connector',
|
||||
href: '/marketplace/openpayd',
|
||||
description:
|
||||
'Operator-provisioned OpenPayd accounts, virtual IBANs, payouts, Open Banking, and gated digital-asset rails for Phoenix Financial Cloud subscribers.',
|
||||
serviceUrl: 'https://openpayd-connector.sankofa.nexus',
|
||||
category: 'Financial messaging',
|
||||
},
|
||||
];
|
||||
|
||||
export const platformServices: ServiceOffering[] = [
|
||||
{
|
||||
|
||||
Reference in New Issue
Block a user