Saleor Store
우리 컬렉션을 쇼핑하세요
아직 감지된 업데이트가 없습니다. GitHub 릴리스가 나오면 여기에 보입니다.
개인 계정의 공개 저장소 소유자는 GitHub로 로그인해 운영자를 확인하고 소개를 직접 관리할 수 있습니다. 관리자 승인은 필요하지 않습니다.
로그인한 뒤 확인 파일을 만들어 이 저장소의 기본 브랜치에 30분 안에 커밋해 주세요. 다른 브랜치나 포크의 파일은 인정하지 않습니다. 파일을 올린 뒤 아래에서 확인하면 관리자 승인 없이 관리할 수 있습니다. 자격을 유지하려면 파일을 남겨 두세요.
로그인이 필요하면 로그인 화면으로 이동합니다. 로그인 후 이곳으로 돌아와 원하는 확인 버튼을 다시 눌러 주세요.
프로젝트 폴더에서 /nomorevibe verify를 실행하면 소유권을 확인하고 소개를 직접 관리할 수 있습니다.
1분 만에 확인하기README 전체를 불러오는 중입니다. 아래는 저장된 발췌입니다.
Neighborhood snack marketplace at neighborhoodcandylady.com.
Approved sellers offer an approved catalog of candy, chips, and drinks for local
pickup. Buyers create an account, pay by card at checkout, and collect with a
pickup code.
• Site: https://www.neighborhoodcandylady.com (static frontend on Vercel)
• API: https://api.neighborhoodcandylady.com (Flask on Render; also candy-lady-api.onrender.com)
• Database: Neon Postgres via DATABASE_URL
• Stripe: live mode on production as of 2026-09-22 (GET /config → stripe_mode: "live")
• Agent ops log: AGENT_WORKLOG.mdREADME 전문 보기 ↗Neighborhood snack marketplace at neighborhoodcandylady.com.
Approved sellers offer an approved catalog of candy, chips, and drinks for local pickup. Buyers create an account, pay by card at checkout, and collect with a pickup code.
candy-lady-api.onrender.com)DATABASE_URLGET /config → stripe_mode: "live")AGENT_WORKLOG.mdindex.html — brand and how it worksbuyer.html — account, shop picker, cart, card checkout, pickup codesshop.html — one shop's own branded page, served at /s/<slug>seller.html — seller login, stock toggles, paid pickup queue, Stripe payouts, shop page settingsadmin.html — admin login, seller approval, catalog, platform earningsapply.html — seller application, including the password the seller will useapplication_fee_amount, with admin totals/s/<slug>, in the shop's own coloursDELETE /me)| Role | How to get one | Can do |
|---|---|---|
| Buyer | "Create account" on buyer.html | Browse shops, pay for orders, see pickup codes and history |
| Seller | Apply on apply.html (choose email + password), then wait for approval | Manage that shop's stock, work its paid pickup queue |
| Admin | manage.py, or the ADMIN_BOOTSTRAP_* env vars | Approve/reject sellers, manage catalog and accounts, see fee earnings |
A seller login works the moment the application is submitted, but the shop stays invisible to buyers until an admin approves it. Roles are enforced in the API, not only in the UI: a buyer token cannot read the approval queue, and a seller token cannot touch another shop's inventory or orders.
The shared prototype password and the "Reset demo" button are gone.
Accounts created before this change have no password. Set one with
python manage.py set-password --email … --password …, or from the admin API
(PUT /users/<id>/password).
DELETE /me, with the account's own token, permanently deletes that login. The
mobile app needs this: App Store guideline 5.1.1(v) requires in-app account
deletion from any app that creates accounts.
curl -X DELETE https://api.neighborhoodcandylady.com/me \
-H "Authorization: Bearer <token>"
The user row is gone, so the email and name are gone with it and the account
cannot log in again. Orders stay, de-identified: a seller can still hand over a
bag that was already paid for, and the platform can still account for the money
it took, but nothing on the order points back to a person. A seller who was the
last login for a shop takes the shop off the buyer-facing list (back to
pending) instead of taking its inventory or other buyers' orders with them.
Admins cannot delete themselves this way; another admin removes them.
backend/README.md has the full breakdown.
The buyer pays on the platform's Stripe Checkout page, and the charge is a
Stripe Connect destination charge: Stripe sends the order total to the
shop's Express account and keeps platform_fee_cents for the platform as
application_fee_amount. Stripe pays sellers; nobody settles up by hand.
A shop can only take card orders once its Stripe account is ready
(charges_enabled). Until then its page says "Card orders coming soon" and the
API answers 409 before reserving any stock — there is no fallback to the
platform collecting the money. Every shop that is live today starts out not
connected, so after this deploys each seller has to click "Connect Stripe" on
seller.html before buyers can pay them again.
Orders placed before Connect keep payout_method: "manual"; /admin/revenue
reports what is still owed on those separately. backend/README.md has the
full Connect, webhook and refund details.
seller.html shows "Stripe Connect is not
enabled on the platform's Stripe account yet".https://api.neighborhoodcandylady.com/stripe/webhook, event
account.updated. Put its signing secret in Render as
STRIPE_CONNECT_WEBHOOK_SECRET. (Optional — the seller page checks status
when Stripe sends the seller back — but it is how later restrictions reach
the app.)apply.html, get approved by an admin (unchanged). Approval also
gives the shop its /s/<slug> link.seller.html, click Connect Stripe and finish Stripe's hosted
onboarding (bank account, identity, tax info — entered at Stripe, never
stored here).seller.html the Payouts panel reads "Ready"; buyers can now pay./s/<slug>)Each approved shop gets a public page with only its own items, name, tagline,
neighborhood, pickup hours and optional logo, in its chosen colours. Sellers
edit the address, tagline, colours and logo link under "Your shop page" on
seller.html, and can copy the link from there. buyer.html still lists every
shop and links each card to its page. Slugs are lowercase kebab-case, 3–48
characters, unique, and cannot be reserved words like admin or privacy.
slug and Connect status are the hooks a guided signup will use)Production (2026-09-22): Render Candy-Lady-api uses live Stripe keys
(pk_live_… / sk_live_…). GET https://api.neighborhoodcandylady.com/config
reports stripe_mode: "live". A live webhook is registered at
https://api.neighborhoodcandylady.com/stripe/webhook for
checkout.session.completed, checkout.session.expired, and charge.refunded.
Still needed for platform payouts: link a USD bank account in the Stripe Dashboard (Balances / settings) so the platform's own fees can be paid out. Each seller links their own bank during Connect onboarding.
Use sk_test_… / pk_test_… only for local development. Never commit keys.
Candy-Lady-apiSet these environment variables (Render dashboard → Environment). Never put keys in the repo.
| Variable | Value |
|---|---|
STRIPE_SECRET_KEY | Production: sk_live_…. Local: sk_test_… |
STRIPE_PUBLISHABLE_KEY | Production: pk_live_…. Local: pk_test_… |
STRIPE_WEBHOOK_SECRET | whsec_… from the live (or test) webhook endpoint |
STRIPE_CONNECT_WEBHOOK_SECRET | optional: whsec_… from the "Connected accounts" endpoint (account.updated) |
PUBLIC_SITE_URL | https://www.neighborhoodcandylady.com |
CORS_ORIGINS | https://www.neighborhoodcandylady.com,https://neighborhoodcandylady.com |
JWT_SECRET_KEY | a long random string |
PLATFORM_FEE_PERCENT | 10 (or whatever commission you want) |
PLATFORM_FEE_FLAT_CENTS | 0 |
Remove PROTOTYPE_LOGIN_PASSWORD.
If STRIPE_SECRET_KEY is missing, placing an order fails with a 503 that names
the variable, and no stock is reserved.
Photos go from the browser straight to an S3-compatible bucket; they never pass through the API. Leave these unset and uploads simply stay switched off — the frontend hides the controls rather than offering a button that cannot work.
| Variable | Value |
|---|---|
STORAGE_ENDPOINT_URL | https://<account-id>.r2.cloudflarestorage.com |
STORAGE_BUCKET | the bucket name, e.g. candy-lady-photos |
STORAGE_ACCESS_KEY_ID | from the R2 API token |
STORAGE_SECRET_ACCESS_KEY | from the same token |
STORAGE_PUBLIC_BASE_URL | the bucket's public URL, e.g. https://photos.neighborhoodcandylady.com |
STORAGE_REGION | auto for R2; a real region for S3 |
MAX_UPLOAD_BYTES | optional, defaults to 8MB |
MAX_SELLER_PHOTOS | optional, defaults to 12 per shop |
REQUIRE_SELLER_IDENTITY | false by default; set true to block approving an unverified shop |
Identity verification reuses STRIPE_SECRET_KEY. There is no separate key.
Two things have to be done outside this repo:
PUT from
https://www.neighborhoodcandylady.com with the Content-Type header.
Without this, uploads fail in the browser and nowhere else.identity.verification_session.verified,
.requires_input and .canceled to the existing webhook endpoint. This is
optional: POST /identity/refresh pulls the verdict on demand, so
verification still completes with no webhook configured. It just means a
seller who closes the tab early has to reopen the page for the result.Nothing about a seller's ID document or selfie is stored here. Stripe collects
both, performs the face match, and returns a verdict; what lands in Postgres is
a status, a vs_… session id, and a timestamp.
Nothing to configure. The frontend reads the publishable key, Stripe mode, and
fee settings from GET /config on the API, so no key ever lives in the static
bundle or in Vercel env vars.
Stripe → Developers → Webhooks → Add endpoint (use live mode for production):
https://api.neighborhoodcandylady.com/stripe/webhookcheckout.session.completed, checkout.session.expired, charge.refunded
(also add Identity events listed above when using seller ID verification)STRIPE_WEBHOOK_SECRETProduction already has this live endpoint as of 2026-09-22.
Without the secret the endpoint returns 503 and refuses unverified calls; the buyer's browser still confirms the payment against Stripe when it returns from Checkout, so the flow works either way. The webhook is what catches a buyer who closes the tab right after paying.
Stripe test mode only:
4242 4242 4242 4242, any future expiry, any CVC, any ZIP4000 0000 0000 0002Run against the deployed site (or locally, see below). You need an admin account and Stripe test keys set.
0. Seller connects Stripe (test mode)
seller.html as a shop's seller. The Payouts panel reads "Not connected" and the shop profile says cards are off.110000000, account 000123456789).seller.html the panel reads "Ready — card orders pay out to you"./s/<slug> link; "Copy link" copies it, "Open page" shows the shop in its colours with only its items.1. Buyer pays and gets a pickup code
buyer.html. The shop grid lists every approved shop with its in-stock count.4242 4242 4242 4242. Stripe returns you to buyer.html, which shows a green "Payment received" banner with a CL-XXXXX pickup code.2. Payment is actually required
buyer.html shows a "Payment cancelled" banner with "Finish paying" and "Release items".3. Seller with a real password fulfills the paid order
seller.html and log in as the shop's seller (the email/password from its application, or one set with manage.py).buyer.html: that item is gone from the shelf and the shop's in-stock count dropped.4. Admin approves a new seller who then logs in
apply.html. Submit a new shop with a fresh email and a password of at least 8 characters.buyer.html does not list the new shop yet.admin.html, log in as admin. The new application is in the review list with its login email.buyer.html: the new shop is now listed.seller.html and log in with the new seller's email and password. The dashboard loads with a full catalog of stock toggles, all out of stock.buyer.html.5. Roles are enforced
buyer.html, open admin.html. It refuses with "That account is not an admin."seller.html with a buyer account.admin.html shows "Platform earnings": paid order count, collected total, platform fee earned, and owed to sellers.6. A buyer deletes their account
The web pages have no delete button yet — this is for the mobile app — so drive
it with curl.
buyer.html, then pay for an order at a shop you can also log into as the seller.localStorage.getItem("candyLadyToken") and copy the token.curl -i -X DELETE https://api.neighborhoodcandylady.com/me -H "Authorization: Bearer <token>" → 204 No Content.buyer.html with that email and password: it fails. Sign up again with the same email and you get a fresh, empty account.seller.html as the shop. The paid order is still in the queue with its pickup code and total, and the buyer's name now reads "Deleted account".Backend (see backend/README.md for details):
cd backend
pip install -r requirements.txt
python manage.py init-db
python manage.py seed-demo # prints demo passwords once
STRIPE_SECRET_KEY=sk_test_... STRIPE_PUBLISHABLE_KEY=pk_test_... \
CORS_ORIGINS=http://localhost:5500 python app.py
Frontend:
python -m http.server 5500
Then open http://localhost:5500/buyer.html. A frontend served from
localhost targets http://127.0.0.1:5000 automatically. To point it somewhere
else, load any page with ?api=https://your-api.example.com (remembered in
localStorage) or set window.CANDY_LADY_API_BASE_URL before app.js runs.
Both overrides work only when the page itself is served from localhost or
127.0.0.1. On any other host, including the live site and Vercel previews,
the frontend always uses https://api.neighborhoodcandylady.com and erases any
override saved by an earlier visit. Otherwise a link carrying ?api= could send
visitors' passwords and session tokens to another server.
Because CORS_ORIGINS includes http://localhost:5500, Stripe returns the
buyer to the local frontend instead of the production domain.
cd backend
python -m pytest tests -q