Commit graph

789 commits

Author SHA1 Message Date
Paulo Castellano
b7374c1adb feat(onboarding): auto-create a default workspace at signup
- CreateUser now provisions a default "{name}'s Workspace" for non-invite
  signups (email, Google, GitHub, seeder), with the owner as Admin and
  current_workspace_id set. Invite signups still skip it (they join the
  inviter's workspace on acceptance).
- UserSeeder drops its now-redundant explicit CreateWorkspace call.
- After Stripe checkout, billing/processing lands the user on the social
  accounts screen with the connect dialog open (was: calendar), so onboarding
  flows straight into connecting an account.

Quantity stays correct (1 workspace → checkout quantity 1). The first workspace
is created without brand details; brand setup remains available in Settings and
on the create-workspace screen for additional workspaces.
2026-06-22 08:18:19 -03:00
Paulo Castellano
17449bfe2a test: cover network_taken popup on OAuth conflict; use FormRequest accessor
- Add a controller-level test asserting the Facebook OAuth callback returns the
  localized network_taken popup (not a generic error) when the network is
  already connected — covers the bug-A fix across the OAuth callbacks.
- WorkspaceController::autofillBrand: use $request->validated('url') instead of
  data_get on the validated array (idiomatic FormRequest accessor).
2026-06-22 08:01:08 -03:00
Paulo Castellano
0619f2a0f4 refactor: address code review for per-workspace pricing
Bug fix
- Surface the localized "network already connected" message on the
  Facebook/Instagram/InstagramFacebook/LinkedInPage/Threads/YouTube OAuth
  callbacks: catch NetworkAlreadyConnectedException before the generic catch
  so the conflict no longer falls through to a generic error + Log::error.

Scope / dead code
- Remove the orphaned BillingController::checkout() + app.billing.checkout route
  (onboarding starts checkout directly); delete the now-dead DiscordWidget and
  useFeatureAccess composable; prune orphaned i18n keys left by removing the
  plan picker / upgrade dialog / count limits (billing.subscribe.*,
  accounts.limit_reached, workspaces.limit_reached, common.discord.*).
- Drop the unused `plan` prop from the usage page and the unused `label` from
  the onboarding persona payload (labels come from i18n); remove
  Persona::options()/label().

Conventions
- declare(strict_types=1) on the two new migrations.
- Extract autofill validation into AutofillBrandRequest (FormRequest).
- Drop the unused $plan param from AccountPolicy::swapPlan.
- CreateUser: drop the stale config('cashier.trial_days', 7) fallback (now 8).
- Rename LimitEnforcementTest to InvitePermissionTest; use Pest mock() helper in
  WorkspaceQuantitySyncTest; move shared test helpers into Pest.php.
- Memoize BillingCycle window() + subscription lookup.
2026-06-21 21:28:47 -03:00
Paulo Castellano
cbf8fb283c feat: per-workspace pricing, onboarding, and billing overhaul
Pricing
- Bill per workspace ($12/mo or $120/yr each); Stripe quantity tracks the
  workspace count and syncs on workspace create/delete.
- 2,500 AI credits per workspace, pooled at the account level; monthly reset
  on the billing anniversary, annual granted upfront (no rollover).
- One social account per network per workspace; remove all count-based limits
  (workspace/social/member) and the legacy plan tiers (single Workspace plan).

Onboarding (cloud only: SELF_HOSTED=false + PostHog)
- Replace the /subscribe plan picker with /onboarding persona selection
  (Creator/Freelancer/Startup/Agency/Small business/Other), saved on the user
  (users.persona) and mirrored to PostHog, then Stripe Checkout on the monthly
  price. 8-day trial so Stripe displays 7.

Billing screen
- Remove the Change Plan dialog (dead with a single plan); add an annual-upgrade
  banner for monthly subscribers (swapToYearly).
- Current-plan card shows the workspace count instead of the plan name.

System AI
- Brand analyzer / workspace autofill is always allowed and never debits credits
  (system feature, not the user's usage).

Self-hosted (SELF_HOSTED=true) bypasses all billing, credit, limit, network,
and onboarding logic.
2026-06-21 20:40:03 -03:00
Paulo Castellano
0a407c4d3b
Merge pull request #104 from trypostit/feat/ai-template-gallery
feat: AI template gallery + tweet-card
2026-06-18 11:55:40 -03:00
Paulo Castellano
31de750c71 fix(automations): persist the Generate node content style
The Generate node data rules had no rule for 'style', so validated()
dropped it on save — the run always fell back to the default image_card.
Add a Rule::in(ContentStyle) rule so the selected style is persisted and
unknown values are rejected.
2026-06-18 11:50:10 -03:00
Paulo Castellano
8ccd287859 feat(ai): collapsible compact ContentStylePicker for tight panels
Add a mini variant that collapses to the selected style and expands into
the list (matching the node config UX), with a larger thumbnail and less
padding so it reads well in the narrow automation panel. The full gallery
on the create page is unchanged.
2026-06-18 11:50:04 -03:00
Paulo Castellano
9f5bacc03f refactor(ai): introduce GeneratorFormat enum and drop stringly-typed format
Replace the 'single'/'carousel' string format threaded through the AI
generation path with a typed GeneratorFormat enum (PostContentGenerator,
PostContentHumanizer, PostContentStreamer, StreamPostCreation,
RunGenerateNode, deriveFormat).

Remove generatorFormat() from AiContentTemplate and the three templates —
it conflated style and format (returning 'tweet_card'). The tweet-card
check now lives on ContentStyle via isTweetCard(), and the humanizer skip
on ContentStyle::humanizes(), so the style declares its own behaviour
instead of callers comparing magic strings.
2026-06-18 11:11:23 -03:00
Paulo Castellano
773d2c729f chore(ai-templates): remove orphaned carousel.png thumbnail
Not referenced anywhere since carousel stopped being a content style.
2026-06-18 11:11:14 -03:00
Paulo Castellano
2cfe817afc feat(ai-templates): regenerate gallery thumbnails with account avatar
Regenerate the three style thumbnails so they show a real account
avatar instead of a blank header. Drop the verified.svg source; only
the rasterized verified.png is read at render time.
2026-06-17 20:32:30 -03:00
Paulo Castellano
7052cee8fb fix(tweet-card): vertically center the name + handle block against the avatar 2026-06-17 20:24:44 -03:00
Paulo Castellano
43b3b7a0c3 feat(tweet-card): larger avatar spanning the name + handle rows 2026-06-17 20:20:27 -03:00
Paulo Castellano
fe0b4ea3d2 refactor(ai-templates): single full-size style card look in both wizard and automation node 2026-06-17 20:12:25 -03:00
Paulo Castellano
f3965b22f1 refactor(ai-templates): shared ContentStylePicker used by wizard + automation node 2026-06-17 20:11:09 -03:00
Paulo Castellano
5df03f775a feat(automations): content-style picker in the generate node config 2026-06-17 20:05:49 -03:00
Paulo Castellano
7fd84507b3 feat(automations): generate node supports the 3 content styles 2026-06-17 20:03:00 -03:00
Paulo Castellano
8d608554cd feat(ai-templates): tweet-card-with-photo thumbnail 2026-06-17 19:53:38 -03:00
Paulo Castellano
0574560570 feat(ai-templates): ContentStyle enum as the typed source for the 3 styles 2026-06-17 19:52:38 -03:00
Paulo Castellano
d8291cb155 feat(ai-templates): tweet-card-with-photo style (blurred dark image background) 2026-06-17 19:43:21 -03:00
Paulo Castellano
b10cef7a9b feat(ai-templates): landscape image-card thumbnail showing the headline (fits the picker frame) 2026-06-17 19:26:36 -03:00
Paulo Castellano
9f7b797674 feat(tweet-card): tabler verified badge (svg source committed) + badge placement next to name 2026-06-17 19:21:19 -03:00
Paulo Castellano
8317b83d6b fix(ai-templates): skip humanizer for tweet_card style regardless of carousel mode
The humanize() guard was keyed on format string 'tweet_card', but for tweet_card
carousel the format becomes 'carousel'. Changed the guard to key on the style key
($this->template === 'tweet_card') so humanization is skipped for all tweet_card
variants — single and carousel alike.
2026-06-17 19:01:10 -03:00
Paulo Castellano
58ac2714d0 feat(ai-templates): styles orthogonal to single/carousel; tweet-card carousel
- TemplateContext gains bool $isCarousel (set from format === CAROUSEL_FORMAT in StreamPostCreation)
- AiContentTemplate interface: promptView() now takes TemplateContext
- ImageCardTemplate handles both single and carousel via context.isCarousel; schema() returns the correct shape per mode; assemble() routes to assembleSingle/assembleCarousel
- TweetCardTemplate handles both single and carousel; promptView() returns tweet_card_carousel view when isCarousel; schema() returns {tweet_text} or {caption, slides[]}; assemble() calls forTweetCard (single) or forTweetCardCarousel (carousel)
- CarouselTemplate deleted — carousel is now a format dimension each style handles, not a standalone style
- AiTemplateRegistry: [ImageCardTemplate, TweetCardTemplate] only
- PostImagePipeline: adds forTweetCardCarousel — renders one renderTweetCard per slide text, returns N media items
- StreamPostCreation: carousel detection restored to $format === CAROUSEL_FORMAT (like main); style dispatched via AiTemplateRegistry::find($this->template); humanize skips tweet_card (single and carousel)
- PostContentGenerator: promptView() call updated to pass templateContext
- New prompt: resources/views/prompts/post_content/tweet_card_carousel.blade.php
- Tests: TemplateContractTest and TweetCardTemplateTest updated for promptView(context) signature; AiTemplateRegistryTest asserts [image_card, tweet_card]; StreamPostCreationTest adds tweet_card carousel test; TweetCardTemplateTest covers both schema shapes and both promptView return values
2026-06-17 18:59:41 -03:00
Paulo Castellano
e886db5621 feat(ai-templates): choose visual style after the format, on the same screen 2026-06-17 18:45:03 -03:00
Paulo Castellano
5184ff3013 feat(ai-templates): real rendered thumbnails for the template picker 2026-06-17 18:34:24 -03:00
Paulo Castellano
bb55e14423 fix(ai-templates): paragraph spacing bug, registry cache, coverage gaps, empty-account UX 2026-06-17 18:09:02 -03:00
Paulo Castellano
24d9653f6e fix(ai-templates): real needsAccount + content_type test coverage; drop unwired humanizableFields; cleanup 2026-06-17 17:56:52 -03:00
Paulo Castellano
799a151321 feat(ai-templates): template picker in the AI wizard 2026-06-17 17:42:23 -03:00
Paulo Castellano
4656cb52c6 feat(ai-templates): i18n template names (en, es, pt-BR) 2026-06-17 17:37:51 -03:00
Paulo Castellano
9d8f772a3e feat(tweet-card): tweet_card template wired end-to-end 2026-06-17 17:35:18 -03:00
Paulo Castellano
0d4f3b90d7 feat(tweet-card): solid-background tweet card image layout 2026-06-17 17:32:10 -03:00
Paulo Castellano
8dfdb2427a feat(tweet-card): prompt + verified badge + thumbnail assets 2026-06-17 17:29:28 -03:00
Paulo Castellano
8c72719f92 feat(ai-templates): validate template key + require account when needed 2026-06-17 17:27:58 -03:00
Paulo Castellano
9dba90129f feat(ai-templates): task 3 - template-driven generator + job
- Add AiTemplateRegistry (resolves, keys, default)
- Add template/templateContext params to PostContentGenerator; schema() delegates to template when both are set; instructions() uses template.promptView()
- Refactor StreamPostCreation.handle() to resolve template via registry, build TemplateContext, delegate assemble() to template; add createPostFromGenerated()
- Remove handleSingle(), handleCarousel(), resolvedContentType() (no other callers)
- PostAiCreateController passes template param from request (default image_card)
- All existing AI tests green; registry test added
2026-06-17 17:23:33 -03:00
Paulo Castellano
97798c010a feat(ai-templates): extract image-card + carousel templates 2026-06-17 17:05:53 -03:00
Paulo Castellano
ebd9a2ac0f feat(ai-templates): content template contract + value objects 2026-06-17 17:03:42 -03:00
Paulo Castellano
34c49c5ce8
Merge pull request #102 from trypostit/chore/release-v1.0.3-artifacts
chore(release): add v1.0.3 changelog artifacts
2026-06-17 09:55:13 -03:00
Paulo Castellano
7dac201ab3 chore(release): add v1.0.3 customer email draft 2026-06-17 09:51:55 -03:00
Paulo Castellano
bdd5829ef3 chore(release): add v1.0.3 changelog artifacts 2026-06-17 09:50:38 -03:00
Paulo Castellano
c1b482acc3
Merge pull request #97 from sebastian-works/main
fix: resolve npm & composer security vulnerabilities
2026-06-16 19:00:50 -03:00
Paulo Castellano
5ccd00ffa2
Merge branch 'main' into main 2026-06-16 18:57:08 -03:00
Paulo Castellano
ce1073918a
Merge pull request #101 from trypostit/feat/mcp-api-platform-meta
Full per-platform meta parity for posts (API + MCP)
2026-06-16 18:14:46 -03:00
Paulo Castellano
40425d7bdc test,refactor: review fixes for per-platform meta parity
- Assert nested Discord meta keys (mention token/label, embed title/color)
  survive validated(), replacing a vacuous coalesced assertion.
- Cover the MCP publish guard for TikTok privacy and Pinterest board, not
  just Discord.
- Use assertUnprocessable() to match sibling API tests; type-hint the API
  UpdatePostRequest withValidator closure.
2026-06-16 18:11:29 -03:00
Paulo Castellano
bf399c80fa docs: document the single-source PostPlatform.meta validation rule
Record in CLAUDE.md that all platforms.*.meta validation and required-on-publish
logic lives only in PostPlatformMetaRules, why (validated() strips unlisted keys),
and where to add new per-platform meta + tests.
2026-06-16 17:52:50 -03:00
Paulo Castellano
ba20534a6d refactor(meta): own the platforms.*.meta parent rule in PostPlatformMetaRules
The shared rules() returned only meta sub-keys, so every caller still repeated
its own 'platforms.*.meta' => array parent (with slightly different sometimes/
nullable combos). Fold the parent into rules() with a single safe contract so
callers just spread one source for the whole meta block.
2026-06-16 17:50:40 -03:00
Paulo Castellano
3aa4b2a2f0 test(api,mcp): cover per-platform meta across networks
Broaden the post meta tests beyond Discord: store persists Instagram aspect
ratio, Pinterest board, TikTok privacy + flags; API update enforces TikTok
privacy and Pinterest board on publish and allows drafts without required meta;
MCP update merges meta and the publish guard ignores disabled platforms.
2026-06-16 17:44:27 -03:00
Paulo Castellano
bdea56d5e1 feat(api,mcp): full per-platform meta parity for posts
Centralize per-platform PostPlatform.meta validation in PostPlatformMetaRules
(shared by web, REST API and MCP). The API and MCP previously only accepted
aspect_ratio, silently stripping channel_id/board_id/privacy_level and the rest
via validated(), so Discord/Pinterest/TikTok couldn't be configured or published
through those entry points. Now all per-platform meta is accepted and persisted,
required-on-publish is enforced on API update (TikTok privacy, Pinterest board,
Discord channel), and the MCP publish tool guards a post's stored meta before
dispatching. Adds API + MCP tests and a PostPlatform discord() factory state.
2026-06-16 17:39:07 -03:00
Paulo Castellano
206d01f77e
Merge pull request #100 from trypostit/feat/discord-channel
feat(channels): add Discord as a social channel
2026-06-16 17:13:47 -03:00
Paulo Castellano
cc8f6625a1 feat(channels): Discord post-level engagement metrics
Add per-post Discord metrics (server member count, thread replies as comments,
and reaction counts) read live with the bot token via GET message + guild
with_counts, wired into PostMetricsFetcher. Redesign the shared post metrics
row: monochrome line icons for member/comment counts vs clean reaction pills,
so audience stats read distinctly from reactions. Applies to Telegram too.
2026-06-16 17:05:13 -03:00
Paulo Castellano
bf0494fbe6 fix(channels): restrict Discord picker to postable channels the bot can use
Only text (0) and announcement (5) channels accept a direct message — forum
(15) and media (16) require thread creation, so they're dropped from the
picker (they returned 'non-text channel'). Channels are also filtered by the
bot's effective VIEW_CHANNEL + SEND_MESSAGES, computed from role base perms and
channel overwrites (Discord's documented algorithm), with a safe fallback to
type-only filtering when the bot's roles can't be resolved.
2026-06-16 16:34:02 -03:00