mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 14:37:26 +00:00
* feat(web): move Hire Us pricing to $400/hour metered billing (PRD 0002) Replaces the $250/week retainer with a $400/hour rate billed against actual hours, invoiced through CoinPay after the client approves them. A 10-hour minimum engagement replaces the week as the unit of commitment. The weekly price lived in 12 places, not the 3 the PRD listed: the front-page Hire Us section, the Top-Level Pages list, /hire-us metadata, /pricing (metadata, two FAQ answers, rate bullet), /about, llms.txt, skill.md, and the Hire Us form success message. Metered billing rather than a committed weekly block, because the old "recurring CoinPay invoice" copy documented a mechanic that never existed: /api/payments/create makes a single one-shot payment, not a subscription. - coinpay-checkout derives amount_usd from hours x 400 instead of a hardcoded 250, validates hours as quarter-hour increments at or above the minimum, and returns 422 before calling CoinPay on bad input. Payment metadata carries billing/hours/rate_usd_per_hour in place of interval. - project-request returns a rate, billing mode, and minimum; no amount exists until hours are approved. - CoinPay config block documents COINPAY_RATE_USD_PER_HOUR / COINPAY_BILLING / COINPAY_MINIMUM_HOURS instead of a weekly amount and interval. - New real /terms route replacing the SPA stub: what is billable, the approve-then-invoice flow, the minimum, cancellation on one week's notice, and an explicit clause that existing engagements keep their terms until both sides agree in writing. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> * fix(mcp): advance prd_next_id expectation to 0003 for PRD 0002 The standards test asserts prd_next_id against the live prd/ directory, so adding prd/0002-hourly-hire-us-rate.md moves the next free id to 0003. This assertion advances with every PRD added to the repo. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com> --------- Co-authored-by: Claude Opus 5 (1M context) <noreply@anthropic.com>
72 lines
4.1 KiB
TypeScript
72 lines
4.1 KiB
TypeScript
import { expect, test } from "@playwright/test";
|
|
|
|
test.describe("LogicSRC PWA", () => {
|
|
test("renders OpenSpec comparison and compatibility commands", async ({ page }) => {
|
|
await page.goto("/openspec");
|
|
|
|
await expect(page.getByRole("heading", { name: "LogicSRC", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "LogicSRC vs OpenSpec.dev" })).toBeVisible();
|
|
await expect(page.getByText("OpenSpec.dev currently positions itself as no-MCP.")).toBeVisible();
|
|
await expect(page.getByText("logicsrc --openspec agentswarm --yolo")).toBeVisible();
|
|
await expect(page.getByText("openspec import")).toBeVisible();
|
|
await expect(page.getByText("openspec export")).toBeVisible();
|
|
});
|
|
|
|
test("renders Credential Sharing OpenSpec route", async ({ page }) => {
|
|
await page.goto("/credential-sharing");
|
|
|
|
await expect(page.getByRole("heading", { name: "Credential Sharing", exact: true })).toBeVisible();
|
|
await expect(page.getByText("Open replacement architecture for secrets")).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: ".env", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Doppler", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "Railway", exact: true })).toBeVisible();
|
|
await expect(page.getByRole("heading", { name: "GitHub Secrets", exact: true })).toBeVisible();
|
|
await expect(page.getByText("logicsrc credentials providers")).toBeVisible();
|
|
});
|
|
|
|
test("renders top-level docs and legal route targets", async ({ page }) => {
|
|
await page.goto("/privacy");
|
|
|
|
await expect(page.getByRole("heading", { name: "Top-Level Pages" })).toBeVisible();
|
|
await expect(page.getByText("/docs · Docs")).toBeVisible();
|
|
await expect(page.getByText("/blog · Blog")).toBeVisible();
|
|
await expect(page.getByText("/credential-sharing · Credential Sharing")).toBeVisible();
|
|
await expect(page.getByText("/terms · Terms")).toBeVisible();
|
|
await expect(page.getByText("/privacy · Privacy")).toBeVisible();
|
|
});
|
|
|
|
test("renders Hire Us project request flow", async ({ page }) => {
|
|
await page.goto("/hire-us");
|
|
|
|
await expect(page.getByRole("heading", { name: "Hire Us", exact: true })).toBeVisible();
|
|
await expect(page.locator(".price-row strong", { hasText: "$400" })).toBeVisible();
|
|
await expect(page.getByText("per hour")).toBeVisible();
|
|
await expect(page.getByText("Ten-hour minimum engagement")).toBeVisible();
|
|
await expect(page.getByText("open infrastructure and open specs for AI agent systems")).toBeVisible();
|
|
await expect(page.getByRole("button", { name: "Request review" })).toBeVisible();
|
|
await expect(page.getByPlaceholder("you@example.com")).toBeVisible();
|
|
await expect(page.getByPlaceholder("Describe the agent workflow")).toBeVisible();
|
|
await expect(page.getByText("without exposing merchant credentials to the browser")).toBeVisible();
|
|
await expect(page.getByText("COINPAY_PRODUCT=logicsrc-hire-us")).toBeVisible();
|
|
await expect(page.getByText("COINPAY_RATE_USD_PER_HOUR=400")).toBeVisible();
|
|
await expect(page.getByText("COINPAY_BILLING=metered_hours")).toBeVisible();
|
|
await expect(page.getByText("COINPAY_STATUS=pending_acceptance")).toBeVisible();
|
|
});
|
|
|
|
test("serves sitemap and RSS XML endpoints", async ({ request }) => {
|
|
const sitemap = await request.get("/sitemap.xml");
|
|
const rss = await request.get("/blog/rss.xml");
|
|
|
|
expect(sitemap.status()).toBe(200);
|
|
expect(sitemap.headers()["content-type"]).toMatch(/(?:application|text)\/xml/);
|
|
await expect(sitemap.text()).resolves.toContain("https://logicsrc.com/openspec");
|
|
await expect(sitemap.text()).resolves.toContain("https://logicsrc.com/credential-sharing");
|
|
await expect(sitemap.text()).resolves.toContain("https://logicsrc.com/hire-us");
|
|
|
|
expect(rss.status()).toBe(200);
|
|
expect(rss.headers()["content-type"]).toMatch(/xml/);
|
|
// Feed is generated from the blog_posts table; the channel header is always
|
|
// present even when there are no posts (e.g. CI without Supabase).
|
|
await expect(rss.text()).resolves.toContain("<title>LogicSRC Blog</title>");
|
|
});
|
|
});
|