Conversation
Pin invoiceshelf/modules to the unreleased 3.4.0 SDK, which is the first
version with Registry::registerAbility(), and contribute the thirteen
abilities the spec's authorization table describes.
The SDK namespaces every module ability as tasks-projects:{ability}, so
Abilities holds the bare names and the registration builds the stored ids
with Registry::abilityId(). Dependencies on a host ability, view-customer
and create-invoice, stay un-namespaced.
The pin goes through an inline composer package repository rather than a
branch alias because the package validator only accepts a plain SemVer
constraint, and a "dev-branch as 3.4.0" alias is not one. Swapping the
block back to a vcs repository is the only change needed once the SDK
tags 3.4.0.
Five reversible migrations behind the tp_ prefix: projects, project members, task statuses, tasks and time entries. Host key widths are not uniform, so every reference column matches its parent by hand, and there are no database-level foreign keys: cascades live in the services. Two indexes carry an invariant rather than just speed. The unique index on tp_tasks (company_id, number) backs the per-company task sequence, and the unique index on tp_time_entries (company_id, running_user_id) enforces one running timer per user per company on MySQL, PostgreSQL and SQLite alike, because all three treat NULLs in a unique index as distinct. Task statuses are not seeded here. A migration runs once per database while companies come and go, and the reversible-migration contract keeps data writes out of up(), so the four defaults are created on demand.
Every business rule lives in app/Application, behind constructor injection, with the models kept dumb. Projects and their members, task statuses and the board, tasks and the fractional drag ordering, time entries and the running timer, the unbilled-to-invoice round trip and the read-only aggregates. The invariants worth naming: - Rounding to the company increment happens when an entry is saved, not at invoice time, so the timesheet and the invoice agree. Zero stays zero and anything above it bills at least one increment. - The rate is resolved once and frozen onto the entry, so changing a task, member, project or company rate never rewrites history. - The board keeps fractional positions and renormalises a column to whole steps when a gap stops being halvable. - The timer holds its one-per-user rule through the unique index, and the race that slips past the read check is reported as the same TimerAlreadyRunning. - Billing refuses a selection spanning two customers or two currencies, treats an entry whose invoice vanished from the host as unbilled again, and confirms idempotently so a half-finished round trip can be replayed. The tests run on Testbench against an in-memory SQLite database, with the three host contracts supplied by fakes in tests/Support.
Tailwind v4 detects content automatically, and explicit @source directives add to that detection rather than replacing it, so the module stylesheet was being built from utility-looking words found anywhere in the repository. Adding backend files moved dist/style.css, which made the CI check that dist is up to date fail on a change that never touched the frontend. source(none) turns the automatic detection off, so only resources/js feeds the scanner and the compiled stylesheet depends on the frontend alone.
Contributor
Author
|
Superseded by #14, which carries this stack consolidated into three commits on top of main. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
M2 of the Tasks and Projects module: the data model, the services that hold the business rules, and the unit tests that pin the invariants. No HTTP surface yet, that is M3.
Tables
Five reversible migrations, all prefixed
tp_, all carryingcompany_id, none with a database-level foreign key. Host key widths are not uniform, socompany_id,user_id,assignee_id,creator_id,currency_id,invoice_id,invoice_item_idandrunning_user_idareunsignedInteger,customer_idisunsignedBigInteger, and module-internal references areunsignedBigInteger.tp_projectsdefault_ratein minor units per hourtp_project_members(project_id, user_id), optional per-member ratetp_task_statusestp_taskscustomer_iddenormalised from the project,board_positiondecimal(20,10), unique on(company_id, number)tp_time_entriesduration_minutes, frozenrateandamount, unique on(company_id, running_user_id)Two of those indexes carry an invariant rather than just speed.
(company_id, number)backs the per-company task sequence, and(company_id, running_user_id)enforces one running timer per user per company on MySQL, PostgreSQL and SQLite alike, since all three treat NULLs in a unique index as distinct: no partial index, no second table.Task statuses are not seeded in a migration. A migration runs once per database while companies come and go, and the reversible-migration contract keeps data writes out of
up(), soTaskStatusService::ensureDefaults()creates Backlog / In Progress / Review / Done the first time a company needs them.Services
All of
app/Application, constructor injection, no business logic in the models and no host Eloquent model anywhere.RoundingstaticroundMinutesRateResolverresolveTaskNumberSequencenextBoardOrderingServicepositionFor,renormaliseBoardQuerycolumnsProjectServicelistFor,findForCompany,create,update,archive,unarchive,delete,totalsProjectMemberServicelistFor,attach,detachTaskStatusServiceensureDefaults,listFor,findForCompany,defaultFor,create,update,reorder,deleteTaskServicelistFor,findForCompany,create,update,delete,moveTimeEntryServicecreate,update,delete,findForCompany,listFor, staticamountForTimerServicerunning,start,stop,discardBillingServiceunbilled,prepare,confirmReportServicesummaryModuleSettingsdefaultRate,roundingMinutes,weekStart,membersSeeAllTimeInvariants the tests pin
TimerAlreadyRunning, and so does the unique-index violation for the race that slips past it; a test forces that race through a model event.preparereturns the host invoice body withgroups[i]aligned toitems[i], hours to two decimals, price in minor units, and the blended rate when a line spans two rates.confirmis idempotent, so a half-finished round trip can be replayed, and refuses an entry stamped to a different invoice.CompanyDataReader::existingInvoiceIds, because the module has no delete hook.findForCompanyexists, and time on an internal project never reaches the billing screen.user_idand render as a removed member.Abilities
Thirteen abilities registered through
Registry::registerAbility(), namespaced by the SDK astasks-projects:{ability}, with the dependency graph from the spec's authorization table. Dependencies on host abilities,view-customerandcreate-invoice, stay un-namespaced.Temporary SDK pin
registerAbility(),registerPageand thecompanyMembers/existingInvoiceIdsreaders are on the unreleased SDK branchfeat/page-route-abilities-members, socomposer.jsonpinsinvoiceshelf/modulesto^3.4.0and satisfies it through an inline composerpackagerepository pointing at commitfb7b629.The obvious
"dev-feat/... as 3.4.0"alias does not work here:validate-packagerequires a plain SemVer constraint and rejects an alias, so the pin has to look like a real version. When the SDK tags 3.4.0, replace the wholerepositoriesblock with the plainvcsentry and nothing else changes. That swap is noted inAGENTS.md.One build fix that rode along
Tailwind v4 detects content automatically and explicit
@sourcedirectives add to that detection rather than replacing it, sodist/style.csswas being built from utility-looking words found anywhere in the repository. Adding backend files moved the compiled stylesheet and broke the CI check thatdistis up to date, on a change that never touched the frontend.source(none)on the utilities import turns the automatic detection off, so onlyresources/jsfeeds the scanner.Verification
composer.lockis deliberately left untracked, matching the other official modules.https://claude.ai/code/session_01DCf36XDKprZifej8dc2r1E