feat(organizations): LinkedIn company-page management (#42) - #126
Merged
Merged
Conversation
…ssignments
An organization's LinkedIn section on the detail screen, shown only to a role
that may manage it: owner and admin. That is the help centre's rule ("when an
organization's owners or admins connect a shared LinkedIn credential") and the
server's — verified live, `PUT .../linkedin/assignments` and
`POST .../linkedin/sync-pages` answer a plain member
`403 {"error":"Admin or owner required"}`. The capability is a new
`OrgPermissions.canManageLinkedIn`, so #83's role matrix stays the one place
roles are decided.
The section reads `GET .../linkedin/status`, assigns a member to a company page
(or clears it) through `PUT .../linkedin/assignments`, refreshes the page list
with `POST .../linkedin/sync-pages`, and disconnects the credential with
`DELETE .../linkedin/credential` behind a confirmation that states the cost: the
organization can no longer post to its pages and every assignment is cleared.
An organization with no credential is a first-class state, not an error. Live,
`GET .../linkedin/status` answers `{"credential":null,"role":"owner"}` for one,
and the 404s `No LinkedIn credential found` / `No active LinkedIn credential for
this organization` mean the same thing — all three render as "not connected"
with nothing to sync or disconnect.
The assignment body is one live-verified `{userId, pageId}` pair per call, not
the "map" the help centre describes: `{}` answers `400 "userId required"`, a
`userId` with no `pageId` answers `200 {"assigned":false}` and so clears that
member's page, an unknown page answers `404 "Page not found in this
organization"`, and a stranger `400 "User is not a member of this
organization"`. `GET` on `.../assignments` and `.../sync-pages` answer 405, so
the documented read endpoints do not exist and the status payload is the only
source of pages and assignments.
The connected payload itself was not observable (no reachable account holds an
org credential), so its wire model is deliberately tolerant: `connected` or the
presence of `credential`, pages and assignments at either the top level or
inside `credential`, and a page read under either field naming.
Tests: status parse for connected and not-connected payloads, the assignment
round-trip asserting the exact request body (and the unassign form that omits
the page), the credential delete and its "none present" answer, sync posting
then re-reading the status, the role gate in `OrgPermissions`, and Compose
coverage that the section is hidden for a member, shown for an owner or admin,
and that disconnecting only fires after the confirmation.
Closes #42
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.
Closes #42. Part of epic #38.
A LinkedIn company-pages section on the organization detail screen, gated on a new
OrgPermissions.canManageLinkedIn— extending #83's matrix rather than adding a second permissionmechanism. It shows credential status, lists discovered pages, assigns a page per member, syncs the
page list, and disconnects behind a confirmation stating the consequence. An organization with no
credential is a first-class state, not an error.
Roles: owner + admin. The help centre says "when an organization's owners or admins connect a
shared LinkedIn credential" — and unlike the member endpoints #83 found unenforced, the server
genuinely enforces it here: a plain member PUTting an assignment or POSTing a sync gets
403 {"error":"Admin or owner required","code":"forbidden"}. Since only management reads the status,the whole section follows the stricter rule and a member never issues the request.
Live findings that contradict the help centre — trust these
GET …/linkedin/status→{"credential":null,"role":"member"}, not the documented{"connected":true,"expiresAt":…}. Non-member →403 "Not a member of this organization".GET …/linkedin/assignmentsandGET …/linkedin/sync-pagesanswer 405 — the help centre'stable lists GETs that do not exist. The status payload is the only possible source of pages and
assignments.
PUT …/linkedin/assignmentstakes one{userId, pageId}pair, not a map.{}→400 "userId required";{"userId":…}with no page →200 {"assigned":false}(clears thatmember); unknown page →
404 "Page not found in this organization"; non-member →400 "User is not a member of this organization".POST …/linkedin/sync-pageswithout a credential →404 "No active LinkedIn credential…";DELETE …/linkedin/credentialwithout one →404 "No LinkedIn credential found". Both render asthe not-connected state.
Could not confirm (no reachable account holds an org credential): the connected payload — the
credential object's fields, the page rows, whether assignments ride on it at all — and the org
sync-pages response body. The DTOs are therefore tolerant (
connectedor presence ofcredential; pages/assignments at top level or insidecredential; page id underid/pageId,name under
pageName/name/label), andsyncLinkedInPagesdeliberately POSTs then re-reads thestatus, so the unknown sync response shape is never depended on.
{"assigned":true}is inferredfrom the observed
false.Verification
./gradlew :app:assembleDebug testDebugUnitTest→ BUILD SUCCESSFUL, 1467 tests, 0 failures. 34new: status parse (connected, not-connected, and the documented top-level shape), assignment
round-trip asserting the exact body
{"userId":"u1","pageId":"p2"}and the unassign form{"userId":"u1"}, page-404 and 403 handling, credential delete and "none present", syncPOST-then-status-GET, the role gate, and the section hidden for a member (status never requested)
/ shown for owner and admin. 9 Compose tests compile, not executed (no emulator).
No
:appchange was needed; nothing outside:feature:organizationswas touched.Live probing — what was created and removed
Created one throwaway organization, "ZZ Android probe 42 (temp)", and ran every write probe
against that org only. Deleted it afterwards; a follow-up GET returns 404.
Against real organizations only reads were sent, plus two calls the server rejected before touching
anything (
PUT …/assignmentswith{}andPOST …/sync-pages, both 403 "Admin or owner required").No credential was removed and no assignment changed on any real organization.
(Independently re-verified: four visible orgs, the probe org 404s.)
Risks
(members already load with
limit=20).assigned" until a page is chosen — assignments set in-session are tracked locally but not
re-derivable on reload. Worth re-probing once an organization actually connects LinkedIn.