mirror of
https://github.com/profullstack/logicsrc.git
synced 2026-08-13 22:37:29 +00:00
feat(web): move Hire Us pricing to $400/hour metered billing (PRD 0002) (#102)
* 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>
This commit is contained in:
parent
da5f6f8381
commit
eaf0a6162b
16 changed files with 446 additions and 43 deletions
|
|
@ -173,18 +173,20 @@ describe("POST /api/hire-us/coinpay-checkout", () => {
|
|||
expect(createCall?.[1]?.headers).toMatchObject({ authorization: "Bearer cp_test_key" });
|
||||
expect(createBody).toMatchObject({
|
||||
business_id: "business-123",
|
||||
amount_usd: 250,
|
||||
amount_usd: 4000,
|
||||
payment_method: "both",
|
||||
currency: "usdc_pol",
|
||||
blockchain: "USDC_POL",
|
||||
description: "LogicSRC Hire Us - $250/week",
|
||||
description: "LogicSRC Hire Us - 10h @ $400/hour",
|
||||
success_url: "https://logicsrc.test/hire-us?payment=success",
|
||||
cancel_url: "https://logicsrc.test/hire-us?payment=cancelled",
|
||||
redirect_url: "https://logicsrc.test/hire-us?payment=coinpay",
|
||||
webhook_url: "https://logicsrc.test/api/webhooks/coinpay",
|
||||
metadata: {
|
||||
product: "logicsrc-hire-us",
|
||||
interval: "week",
|
||||
billing: "metered_hours",
|
||||
hours: 10,
|
||||
rate_usd_per_hour: 400,
|
||||
source: "logicsrc.com/hire-us",
|
||||
buyer_email: "buyer@example.com"
|
||||
}
|
||||
|
|
@ -241,7 +243,71 @@ describe("POST /api/hire-us/coinpay-checkout", () => {
|
|||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(body.payment.amount_usd).toBe(250);
|
||||
expect(body.payment.amount_usd).toBe(4000);
|
||||
});
|
||||
|
||||
it("prices the checkout from the approved hours", async () => {
|
||||
process.env.COINPAY_API_KEY = "cp_test_key";
|
||||
process.env.COINPAY_API_URL = "https://coinpayportal.example";
|
||||
process.env.COINPAY_BUSINESS_ID = "business-123";
|
||||
process.env.COINPAY_ELIGIBILITY_MERCHANT_ID = "merchant-123";
|
||||
process.env.COINPAY_HIRE_US_BLOCKCHAIN = "USDC_POL";
|
||||
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch").mockImplementation(async (input) => {
|
||||
const url = typeof input === "string" ? input : input.toString();
|
||||
if (url.includes("/api/payments/merchant-eligibility")) {
|
||||
return jsonResponse({ success: true, accepts_card: true, accepts_crypto: true, chains: ["USDC_POL"] });
|
||||
}
|
||||
return jsonResponse({ success: true, payment: { id: "pay_123", amount_usd: 10100 } }, 201);
|
||||
});
|
||||
|
||||
const response = await coinpayCheckout(
|
||||
new NextRequest("http://localhost/api/hire-us/coinpay-checkout", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ hours: 25.25 })
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
const createCall = fetchMock.mock.calls.find(([input]) =>
|
||||
(typeof input === "string" ? input : input.toString()).includes("/api/payments/create")
|
||||
);
|
||||
const createBody = JSON.parse((createCall?.[1]?.body as string) ?? "{}");
|
||||
|
||||
expect(response.status).toBe(201);
|
||||
expect(createBody.amount_usd).toBe(10100);
|
||||
expect(createBody.description).toBe("LogicSRC Hire Us - 25.25h @ $400/hour");
|
||||
expect(createBody.metadata).toMatchObject({ billing: "metered_hours", hours: 25.25, rate_usd_per_hour: 400 });
|
||||
expect(body.payment).toMatchObject({ amount_usd: 10100, hours: 25.25, rate_usd_per_hour: 400 });
|
||||
});
|
||||
|
||||
it("rejects hours below the minimum engagement or off the quarter-hour", async () => {
|
||||
process.env.COINPAY_API_KEY = "cp_test_key";
|
||||
process.env.COINPAY_API_URL = "https://coinpayportal.example";
|
||||
process.env.COINPAY_BUSINESS_ID = "business-123";
|
||||
process.env.COINPAY_ELIGIBILITY_MERCHANT_ID = "merchant-123";
|
||||
|
||||
const fetchMock = vi.spyOn(globalThis, "fetch");
|
||||
|
||||
for (const hours of [9.75, 12.3, "many", -40]) {
|
||||
const response = await coinpayCheckout(
|
||||
new NextRequest("http://localhost/api/hire-us/coinpay-checkout", {
|
||||
method: "POST",
|
||||
headers: { "content-type": "application/json" },
|
||||
body: JSON.stringify({ hours })
|
||||
})
|
||||
);
|
||||
const body = await response.json();
|
||||
|
||||
expect(response.status).toBe(422);
|
||||
expect(body).toEqual({
|
||||
success: false,
|
||||
error: "Approved hours must be a quarter-hour increment of at least 10"
|
||||
});
|
||||
}
|
||||
|
||||
expect(fetchMock).not.toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it("does not create checkout when no payment rail is available", async () => {
|
||||
|
|
@ -290,7 +356,13 @@ describe("POST /api/hire-us/project-request", () => {
|
|||
expect(response.status).toBe(202);
|
||||
expect(body).toMatchObject({
|
||||
success: true,
|
||||
request: { status: "pending_acceptance", amount_usd: 250, interval: "week", invoice: "created_after_acceptance" }
|
||||
request: {
|
||||
status: "pending_acceptance",
|
||||
rate_usd_per_hour: 400,
|
||||
billing: "metered_hours",
|
||||
minimum_hours: 10,
|
||||
invoice: "created_after_acceptance"
|
||||
}
|
||||
});
|
||||
expect(body.request.id).toMatch(/^hire_/);
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue