# What several assignments compete for — shared pools, claims, and what happens when there is less than was thought (Assignments.Allocations kit)

> Assignments opened under one outcome often draw on the same thing — the stock in one warehouse, a line's hours, a sum of money. A shared pool says how much there is. A claim on it is held for one assignment or refused, so two assignments never hold the same unit; a claim that does not fit can wait its turn. When a count finds less than is held, the pool is overdrawn: every holder is told its share is in question, nothing of it can be used, and whoever keeps the pool decides who gives way. An assignment that closes holds nothing. The pool has a card, and every assignment's page says what it holds of what is shared.

<!-- id: agent-assignment-allocations · area: agent · stability: preview · html: https://osysharp.com/reference/agent/assignment-allocations/ -->

## Summary        {#summary}
```osy syntax
app Pumpworks {
  use Osysharp.Assignments;
  use Osysharp.Assignments.Allocations;
}

policy KeepsSharedPools      => IsOperations;                  // says what there is; decides who gives way
policy ClaimsFromSharedPools => IsOperations || IsRegionLead;  // sees the pools; claims for a job they act on

var stock = OpenPool(new NewPool { Key = "malmo-housings", Title = "Housings in the Malmö warehouse", Unit = "housings", Available = 120 });
Claim(stock, nordics, 80, "Nordic orders due this month");     // held: nobody else can take these 80
Claim(stock, dach, 60, "DACH orders due this month");          // refused — 40 are left, and it says what the rest is held for
```

## Signature      {#signature}
```osy syntax
SharedPool OpenPool(NewPool what);                              // KeepsSharedPools — or the pool already open under the key
SharedPool PoolBy(string key);
decimal    LeftIn(SharedPool pool);

Allocation Claim(SharedPool pool, Assignment a, decimal amount, string why, string key = "");   // held, or throws NotEnoughLeft
Allocation AskFor(SharedPool pool, Assignment a, decimal amount, string why, string key = "");  // held, or waits its turn
void ReleaseAllocation(Allocation claim, string why);           // given back; a waiting claim is withdrawn
void UseAllocation(Allocation claim, string what);              // shipped, spent, worked: gone from the pool for good

void CorrectPool(SharedPool pool, decimal available, string why);    // KeepsSharedPools — what there is now
void CutAllocation(Allocation claim, decimal to, string why);        // KeepsSharedPools — who gives way; 0 takes all of it

PoolView             PoolOf(Guid pool);
List<AllocationLine> AllocationsOf(Guid assignment);
component PoolCard(Guid pool);
```

## Description    {#description}

### What is a pool, and what is a claim?   {#pool}
A **pool** is something there is a counted amount of: `Available`, in a `Unit` a person reads ("housings", "hours",
"EUR"). A **claim** is one assignment's share of it: an amount, what it is for, and where it stands — held, waiting,
given back, used, taken away.

`Claim` either holds the amount at once or throws `NotEnoughLeft`, whose message says how much is left and what the
rest is held for:

> 40 housings of Housings in the Malmö warehouse are left, and “DACH” asked for 60 housings. Held: 80 housings for
> Nordic orders due this month.

Claimed again under the same `key`, the claim already made is answered — a process step that is run again after a
crash holds nothing twice.

### Why does a claim say what it is for?   {#why}
Whoever sees a pool need not be on the assignments that draw on it: a region's lead reads her own job and not her
neighbour's. So a claim is named, to everybody who sees the pool, by what its claimant said it is for — `why` — and
an assignment's title is shown only to somebody who may read that assignment. Write `why` for those readers.

### What if it does not fit, and the assignment can wait?   {#wait}
`AskFor` holds the amount when there is room. Otherwise the claim **waits its turn**: the assignment carries a piece
of blocked work — "Wait for 30 housings of Housings in the Malmö warehouse" — which is what makes its page say it
cannot move, and why. Waiting claims are served in the order they were made, when room appears: something is given
back, used up elsewhere, or a delivery is counted in. Nobody jumps the queue — with somebody already waiting, a new
claim waits behind them even if it would fit.

Whoever waits is served by the app itself, never by whoever made the room: they are seldom on the assignment that
waited.

### What happens when there is less than was thought?   {#overdrawn}
`CorrectPool(pool, available, why)` records what there is now. With more room, whoever waits is served. With
**less than is held**, the pool is *overdrawn*, and the kit takes nothing from anybody:

- every assignment holding some of it gets a line on its activity: how much there is now, how much is held, and
  that its own share is in question;
- a share that is in question cannot be used (`UseAllocation` refuses);
- `PoolOverdrawn` is raised, for the app to tell whoever decides.

Who gives way is a person's decision: `CutAllocation(claim, to, why)`, by whoever keeps the pool. The assignment is
told, with the reason, and `AllocationCut` is raised — which is where an app puts that assignment's plan back in
question and plans again. When no more is held than there is, nobody's share is in question any more.

```osy syntax
[On] void ARegionGaveWay(AllocationCut e) {
  var region = SalesRegion.Single(r => r.Job == e.Allocation.Assignment);
  ReopenCriterion(CriterionOf(region.Job, "plan"), "Its share of the stock was cut: " + e.Why);
}
```

### What happens when an assignment closes?   {#closes}
It holds nothing. Completed, called off or failed, what it held goes back to the pool, what it waited for is
withdrawn — with the reason the assignment gave — and whoever was waiting is served in turn.

### Who may do what?   {#who}
| | |
|---|---|
| Open a pool, say what there is now, decide who gives way | `KeepsSharedPools` — nobody until the app says |
| See the pools; claim, give back and use, for an assignment they act on | `ClaimsFromSharedPools` — nobody until the app says |
| Read a claim | whoever sees the pools, and whoever is on its assignment |
| Write a pool's or a claim's numbers by hand | nobody — the keeper included |

Two claims made at the same moment cannot both pass: the pool's held amount is checked on every write
(`[ConcurrencyCheck]`), and the second is refused as a conflict.

### What is on screen?   {#screens}
`PoolCard(pool)` draws the pool: how much there is, how much is held and how much is left; each holder, with what
it holds and whether its share is in question; who waits their turn; and what was given back, used or taken away,
with why. Overdrawn, it says so first, and by how much.

Every assignment's page (`AssignmentView`) gains a section, "Shared resources", with what that assignment holds and
waits for.

## Examples       {#examples}
The kit's test app is a pump maker whose supplier of housings shuts down — [A supplier shuts down — three regions, one stock, a plan refused by the stock, and a plan made again](https://osysharp.com/reference/agent/story-disruption/) tells it. Its
tests show each rule:

| | |
|---|---|
| A claim is held, and one that does not fit is refused saying what the rest is held for | `a_claim_is_held_and_one_that_does_not_fit_is_refused_saying_who_holds_the_rest` |
| Claimed again under the same key, it is the claim already made | `a_claim_made_again_under_the_same_key_is_the_claim_already_made` |
| A claim that does not fit can wait, and is served in the order it asked | `a_claim_that_does_not_fit_can_wait_and_is_served_in_the_order_it_asked` |
| A job that is closed holds nothing, and whoever waited is served | `a_job_that_is_closed_holds_nothing_and_whoever_waited_is_served` |
| Less than was thought overdraws the pool, and its keeper decides who gives way | `less_than_was_thought_overdraws_the_pool_and_its_keeper_decides_who_gives_way` |
| Each person does what is theirs with a pool, and nothing else | `each_person_does_what_is_theirs_with_a_pool_and_nothing_else` |

```osy title="two jobs share one stock: a claim is held, one is refused, one waits, and less than was thought is decided by the keeper" run app=agent-assignment-allocations
app Pumpworks {
  use Osysharp.Assignments;
  use Osysharp.Assignments.Allocations;
  use Osysharp.Llm;
  use Osysharp.Identity;
  use Osysharp.Observability;
  use Osysharp.Workflow;
  use Osysharp.Ui;
  model "model/**/*.osy";
}

using Osysharp.Assignments;
using Osysharp.Assignments.Allocations;
using Osysharp.Identity;

[Role] enum PlantRole { Authenticator, Operations, RegionLead }
partial entity RoleGrant { security { allow read when IsAuthenticated; } }
policy IsOperations => RoleGrant.Any(g => g.Member == user && g.Role == PlantRole.Operations);
policy IsRegionLead => RoleGrant.Any(g => g.Member == user && g.Role == PlantRole.RegionLead);

// The kits' questions, answered by this app.
policy OpensAssignments            => IsOperations;
policy OverseesAssignments         => IsOperations;
policy AcceptsAssignmentExceptions => IsOperations;
policy KeepsSharedPools            => IsOperations;
policy ClaimsFromSharedPools       => IsOperations || IsRegionLead;

entity Person : Principal {
  security { allow read when IsAuthenticated || IsAuthenticator; allow create when IsAuthenticator; }
}

[TestFixture]
void Plant() {
  var astrid = new Person { Email = "astrid@pumpworks.example", DisplayName = "Astrid" };
  var bram   = new Person { Email = "bram@pumpworks.example",   DisplayName = "Bram" };
  var clara  = new Person { Email = "clara@pumpworks.example",  DisplayName = "Clara" };
  new RoleGrant { Member = astrid, Role = PlantRole.Operations };
  new RoleGrant { Member = bram,   Role = PlantRole.RegionLead };
  new RoleGrant { Member = clara,  Role = PlantRole.RegionLead };
}
principal Astrid => Person.Single(p => p.Email == "astrid@pumpworks.example");
principal Bram   => Person.Single(p => p.Email == "bram@pumpworks.example");
principal Clara  => Person.Single(p => p.Email == "clara@pumpworks.example");

SharedPool Stock() { return PoolBy("malmo-housings"); }
Assignment Job(string title) { return Assignment.Single(a => a.Title == title); }
Allocation ClaimFor(string title) { return Allocation.Single(c => c.Assignment.Title == title); }

[Test(Plant)]
void Two_jobs_share_one_stock_and_the_keeper_decides_who_gives_way() {
  runas (Astrid) {
    OpenPool(new NewPool { Key = "malmo-housings", Title = "Housings in the Malmö warehouse", Unit = "housings", Available = 120 });
    OpenAssignment(new NewAssignment { Title = "Nordics", Goal = "Nordic orders ship.", Criteria = ["Shipped"], Owner = Bram });
    OpenAssignment(new NewAssignment { Title = "DACH", Goal = "DACH orders ship.", Criteria = ["Shipped"], Owner = Clara });
  }
  runas (Bram) {
    Assert.Equal(AllocationState.Held, Claim(Stock(), Job("Nordics"), 80, "Nordic orders due this month").State);
  }
  runas (Clara) {
    // 60 does not fit: the refusal says what is left and what the rest is held for.
    var refused = Assert.Throws<NotEnoughLeft>(() => Claim(Stock(), Job("DACH"), 60, "DACH orders due this month"));
    Assert.Equal(40, refused.Left);
    // Asked for instead, the claim waits its turn.
    Assert.Equal(AllocationState.Waiting, AskFor(Stock(), Job("DACH"), 60, "DACH orders due this month").State);
  }

  // A count finds less than is held. Nothing is taken from anybody; the keeper decides.
  runas (Astrid) {
    CorrectPool(Stock(), 70, "The count found 50 housings damaged by damp.");
    Assert.True(PoolOf(Stock().Id).Overdrawn);
  }
  runas (Bram) { Assert.Throws<ValidationException>(() => UseAllocation(ClaimFor("Nordics"), "Shipped")); }
  runas (Astrid) { CutAllocation(ClaimFor("Nordics"), 10, "Most Nordic orders can ship in June."); }
  Background.Settle();

  // No more is held than there is, and the room the cut made went to whoever waited.
  runas (Astrid) {
    Assert.False(PoolOf(Stock().Id).Overdrawn);
    Assert.Equal(AllocationState.Held, ClaimFor("DACH").State);
    Assert.Equal(70, Stock().Held);
  }

  // A job that closes holds nothing: what it held goes back to the pool.
  runas (Bram) { CancelAssignment(Job("Nordics"), "The Nordic customers took another pump."); }
  Background.Settle();
  runas (Astrid) {
    Assert.Equal(AllocationState.Released, ClaimFor("Nordics").State);
    Assert.Equal(60, Stock().Held);
  }
}
```

## See also       {#see-also}
- [Assignments — a piece of work owned until its outcome is reached (Assignments kit)](https://osysharp.com/reference/agent/assignments/) — the assignment a claim is made for; jobs opened under a job, and links between them
- [What an assignment asks of people — requests as an assignment's work and as inbox items (Assignments.WorkQueues kit)](https://osysharp.com/reference/agent/assignment-requests/) — what an assignment asks of people
- [A supplier shuts down — three regions, one stock, a plan refused by the stock, and a plan made again](https://osysharp.com/reference/agent/story-disruption/) — three regions, one stock, and a plan made again
