company 2.0.0 → 3.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +34 -0
- data/README.md +30 -9
- data/lib/company/account.rb +3 -0
- data/lib/company/collection.rb +9 -2
- data/lib/company/collections/windows.rb +16 -0
- data/lib/company/resources/visit.rb +9 -4
- data/lib/company/resources/window.rb +16 -0
- data/lib/company/version.rb +1 -1
- data/lib/company.rb +2 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 49d635d99039ba1f2dd9e2fc68794b7430b49890b9ccfdacd34c420ed7d38e6d
|
|
4
|
+
data.tar.gz: 0d4e6da3fae824c5db9af413d5be87b94db532c336bccd1194896381eb5245cd
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: '0524899dee91ba36a54f37ca834cd770e27a202ae3b65e79efc7aeb9bb889697184ec186724a62c015f589a51e27c866aad7ef27c8dfebc3f67b62fbd491aa86'
|
|
7
|
+
data.tar.gz: ff4a532ba721b64b33bab772235c17c5f3ea2bdc15622c4cd9bd4e7b3ef9d361c15862cecc9f667b4d9b84f28f8fe8f4cae6d0b2f96c7dff828c562906479028
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,40 @@ All notable changes to this project will be documented in this file.
|
|
|
5
5
|
For more information about changelogs, check [Keep a Changelog](http://keepachangelog.com) and
|
|
6
6
|
[Vandamme](http://tech-angels.github.io/vandamme).
|
|
7
7
|
|
|
8
|
+
## 3.0.0 - 2026-09-16
|
|
9
|
+
|
|
10
|
+
- [Breaking change] `Collection#assigned_to` is `Collection#of`. It read as it should on booked
|
|
11
|
+
time -- a visit is assigned to somebody -- and as it should not on free time, nobody being
|
|
12
|
+
assigned to an hour they are not working. `account.visits.of(technician)` and
|
|
13
|
+
`account.windows.of(technician)` both read as the possessive they are.
|
|
14
|
+
|
|
15
|
+
- [Feature] `account.windows`, a `Company::Windows` of `Company::Window`: the free time of a
|
|
16
|
+
business, every stretch somebody could be sent out in, over the window asked for. A window
|
|
17
|
+
reads `starts_at` and `ends_at` and nothing else -- it names no work and nobody going, a list
|
|
18
|
+
of them having been narrowed to one technician already.
|
|
19
|
+
|
|
20
|
+
- [Feature] A window is as long as it is, rather than cut into slots of a fixed size. Only the
|
|
21
|
+
caller making an offer knows how long the pieces it offers need to be, and a stretch stored
|
|
22
|
+
whole can still be cut, where one already cut cannot be put back together.
|
|
23
|
+
|
|
24
|
+
- [Note] A platform that does not work free time out for itself raises `NotImplementedError`
|
|
25
|
+
from `windows`, and `Company::Windows#of` does the same, rather than answering with none. An
|
|
26
|
+
empty week and a fully booked one are the same shape, so a caller reading none as none would
|
|
27
|
+
quietly stop offering that business at all -- with nothing in a log and no page looking wrong.
|
|
28
|
+
|
|
29
|
+
## 2.1.0 - 2026-09-15
|
|
30
|
+
|
|
31
|
+
* [Feature] `Company::Collection#includes(*names)`, what to bring back beside each record. A
|
|
32
|
+
platform that charges for what a row carries answers it by asking for more; one that hands a
|
|
33
|
+
record over whole has nothing to ask for and answers the same list. Either way a caller names
|
|
34
|
+
what it reads without knowing which kind of platform it is talking to, which a caller sharing
|
|
35
|
+
one code path across two of them could not do before
|
|
36
|
+
* [Feature] `Company::Visit#location`, where the stop is. A schedule is read to know who is
|
|
37
|
+
where and when, so a visit says where without being asked what it was booked for, and a
|
|
38
|
+
caller no longer reaches through `visit.job` for an address -- which a stop booked against a
|
|
39
|
+
lead, or an hour blocked out against nothing, could never answer. `job` and `lead` say why a
|
|
40
|
+
visit exists and either may be absent; `location` may be too, for booked time that is nowhere
|
|
41
|
+
|
|
8
42
|
## 2.0.0 - 2026-09-15
|
|
9
43
|
|
|
10
44
|
* [Breaking change] `Company::Selection` takes the rule to keep records by as a block rather
|
data/README.md
CHANGED
|
@@ -79,12 +79,14 @@ visit.description # => 'Service appointment'
|
|
|
79
79
|
visit.starts_at # => 2026-09-10 01:00:00 UTC
|
|
80
80
|
visit.ends_at # => 2026-09-10 02:00:00 UTC
|
|
81
81
|
visit.anytime? # => false
|
|
82
|
-
visit.
|
|
83
|
-
visit.
|
|
82
|
+
visit.location # => #<Acme::Location>, or nil where it is nowhere
|
|
83
|
+
visit.job # => job, or nil where no job was booked for it
|
|
84
|
+
visit.lead # => lead, or nil where no lead was booked for it
|
|
84
85
|
visit.technicians # => [#<Acme::Technician>, ...]
|
|
85
86
|
|
|
86
87
|
visits.for_jobs # => only the stops of jobs
|
|
87
88
|
visits.for_leads # => only the stops of leads
|
|
89
|
+
visits.includes(location: :customer) # => the same list, whatever the platform charges
|
|
88
90
|
|
|
89
91
|
booked = account.visits.create name: 'Jane', surname: 'Qi', phone: '5555555666',
|
|
90
92
|
email: 'jane@example.com', address: { street: '100 Acme Circle', zip: '98920' },
|
|
@@ -98,9 +100,13 @@ technician.id # => 't1'
|
|
|
98
100
|
technician.name # => 'Grace'
|
|
99
101
|
technician.surname # => 'Hopper'
|
|
100
102
|
|
|
101
|
-
week = account.visits.between(monday, sunday).
|
|
103
|
+
week = account.visits.between(monday, sunday).of(technician)
|
|
102
104
|
week.ids # => ['v1', 'v2', ...]
|
|
103
105
|
|
|
106
|
+
free = account.windows.between(monday, sunday).of(technician)
|
|
107
|
+
free.first.starts_at # => 2026-09-16 13:00:00 UTC
|
|
108
|
+
free.first.ends_at # => 2026-09-16 17:00:00 UTC
|
|
109
|
+
|
|
104
110
|
lead = account.leads.create name: 'Jane', surname: 'Qi', phone: '5555555666',
|
|
105
111
|
email: 'jane@example.com', address: { street: '100 Acme Circle', zip: '98920' },
|
|
106
112
|
description: 'Repair', notes: 'Estimate $20–$30', source: 'Website'
|
|
@@ -114,12 +120,25 @@ far as it goes or narrowed: to a window measured from now, to the technician the
|
|
|
114
120
|
booked for, or to one kind of stop. The narrowings compose in any order, so one technician's
|
|
115
121
|
week reads the same whichever is asked for first.
|
|
116
122
|
|
|
117
|
-
A visit is any booked time
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
123
|
+
A visit is any booked time: somebody is somewhere for an hour. That is what a schedule is read
|
|
124
|
+
for -- who is where, and when -- so a visit says where it is without being asked what it was
|
|
125
|
+
booked for, and a caller never reaches through a job to find an address.
|
|
126
|
+
|
|
127
|
+
`job` and `lead` answer *why*, and either may be absent. A stop to look at something nobody has
|
|
128
|
+
priced yet -- Jobber calls it an assessment, Housecall Pro an estimate -- names a `lead` and no
|
|
129
|
+
`job`. An hour blocked out on a calendar names neither, and occupies the technician's day just
|
|
130
|
+
the same. `quote` stays the price, which is the other half of what Housecall Pro files as one
|
|
121
131
|
record.
|
|
122
132
|
|
|
133
|
+
A window is the other half of the same question: not the hours somebody is out, but the ones
|
|
134
|
+
they are not. It names no work and nobody going, a list of them having been narrowed to one
|
|
135
|
+
technician already, and it is as long as it is -- cut it into offerable pieces where an offer is
|
|
136
|
+
being made, since only the caller making one knows how long it needs them to be.
|
|
137
|
+
|
|
138
|
+
A platform that does not work free time out for itself answers `windows` with
|
|
139
|
+
`NotImplementedError` rather than with none. An empty week and a fully booked one are the same
|
|
140
|
+
shape, so a caller reading none as none would quietly stop offering that business at all.
|
|
141
|
+
|
|
123
142
|
## Answering as a gem
|
|
124
143
|
|
|
125
144
|
A gem subclasses `Company::Account` and answers the readers its platform offers, and subclasses
|
|
@@ -143,8 +162,10 @@ end
|
|
|
143
162
|
|
|
144
163
|
`Company::Business.node_keys` then answers `[:id, :name, :phone_number]`: exactly what to ask
|
|
145
164
|
the platform for. A reader the gem leaves out raises `NotImplementedError` naming the gem; leads
|
|
146
|
-
it leaves out refuse to file one, and so do the visits. `
|
|
147
|
-
|
|
165
|
+
it leaves out refuse to file one, and so do the visits. `includes` a gem leaves out answers the
|
|
166
|
+
same list: a platform that hands a record over whole has nothing to bring back beside it, so a
|
|
167
|
+
caller names what it reads without knowing which kind of platform it is talking to. `of`,
|
|
168
|
+
`for_jobs` and `for_leads` a gem leaves out still answer: `Company::Selection` walks the list and lets through
|
|
148
169
|
what the rule keeps, so only a platform that can put the question to its server writes the
|
|
149
170
|
method, and only to save the requests the walk would spend. The least a gem writes is under `test/acme`, and the test that
|
|
150
171
|
runs every reader through it, `test/company/acme_test.rb`, reads as a tutorial.
|
data/lib/company/account.rb
CHANGED
|
@@ -15,6 +15,9 @@ module Company
|
|
|
15
15
|
# @return [Collection] technicians of the business, each a {Technician}.
|
|
16
16
|
def technicians = unanswered :technicians
|
|
17
17
|
|
|
18
|
+
# @return [Windows] free time of the business, each a {Window}, where the platform works it out.
|
|
19
|
+
def windows = unanswered :windows
|
|
20
|
+
|
|
18
21
|
# @return [Leads] leads of the business: `create` files one, where the platform takes leads.
|
|
19
22
|
def leads = Leads.new
|
|
20
23
|
|
data/lib/company/collection.rb
CHANGED
|
@@ -20,11 +20,18 @@ module Company
|
|
|
20
20
|
between within && now - within, now
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# What to bring back beside each record, for a platform that charges for the asking. One
|
|
24
|
+
# that answers a record whole has nothing to ask for and answers the same list, so a caller
|
|
25
|
+
# names what it reads without knowing which kind of platform it is talking to.
|
|
26
|
+
# @param names [Array<Symbol, Hash>] what to read beside each record, as the gem names them.
|
|
27
|
+
# @return [Collection] the same list, bringing those back where that costs anything.
|
|
28
|
+
def includes(*names) = self
|
|
29
|
+
|
|
23
30
|
# A platform that can ask its server for one technician's work narrows the list there; one
|
|
24
31
|
# that cannot walks the list and keeps what the technician turns out to be on.
|
|
25
32
|
# @param technician [Technician] whoever the work is booked for.
|
|
26
|
-
# @return [Collection] the same list, narrowed to
|
|
27
|
-
def
|
|
33
|
+
# @return [Collection] the same list, narrowed to theirs.
|
|
34
|
+
def of(technician)
|
|
28
35
|
Selection.new(collection: self) do |record|
|
|
29
36
|
record.technicians.any? { |each| each.id == technician.id }
|
|
30
37
|
end
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Company
|
|
2
|
+
# The free time of a business: every stretch somebody could be sent out in, over the window
|
|
3
|
+
# asked for. A platform that works it out answers this; one that does not know when its
|
|
4
|
+
# business is open cannot, and says so rather than answering with none, an empty list being
|
|
5
|
+
# the same shape as a fully booked week.
|
|
6
|
+
class Windows < Collection
|
|
7
|
+
# A window names nobody, a list of them having been narrowed to one technician already, so
|
|
8
|
+
# there is nothing here to walk and keep: only the platform can tell one person's free time
|
|
9
|
+
# from another's.
|
|
10
|
+
# @param technician [Technician] whose free time to answer.
|
|
11
|
+
# @return [Windows] the same list, as that technician's alone.
|
|
12
|
+
def of(technician)
|
|
13
|
+
raise NotImplementedError, "#{self.class} does not answer one technician's windows"
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
module Company
|
|
2
|
-
#
|
|
3
|
-
# booked
|
|
2
|
+
# Booked time: somebody is somewhere for an hour. Where it is, who is going and when are what
|
|
3
|
+
# a visit is for; the job or the lead it was booked against says why, and either may be absent.
|
|
4
4
|
class Visit < Resource
|
|
5
5
|
# What every visit reads, by the vocabulary's names.
|
|
6
6
|
def self.attributes = %i[id description starts_at ends_at anytime]
|
|
@@ -17,10 +17,15 @@ module Company
|
|
|
17
17
|
# @return [Boolean, nil] whether the visit may happen any time that day rather than at an hour.
|
|
18
18
|
def anytime? = attribute :anytime
|
|
19
19
|
|
|
20
|
-
#
|
|
20
|
+
# A stop says where it is without being asked what it was booked for, so a caller reading a
|
|
21
|
+
# schedule never has to reach through a job to find an address.
|
|
22
|
+
# @return [Location, nil] where the work happens, nil where the stop is booked nowhere.
|
|
23
|
+
def location = record Location, :location
|
|
24
|
+
|
|
25
|
+
# @return [Job, nil] job the stop belongs to, nil where no job was booked for it.
|
|
21
26
|
def job = record Job, :job
|
|
22
27
|
|
|
23
|
-
# @return [Lead, nil] lead the stop belongs to, nil where
|
|
28
|
+
# @return [Lead, nil] lead the stop belongs to, nil where no lead was booked for it.
|
|
24
29
|
def lead = record Lead, :lead
|
|
25
30
|
|
|
26
31
|
# @return [Array<Technician>] whoever the stop is booked for, where the platform names them.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
module Company
|
|
2
|
+
# Free time: a stretch nobody is booked for, which somebody could be sent out in. It names no
|
|
3
|
+
# work and nobody going, a list of them having been asked for one technician already, and it
|
|
4
|
+
# is as long as it is -- cut it into offerable pieces where an offer is being made, since only
|
|
5
|
+
# the caller making one knows how long it needs them to be.
|
|
6
|
+
class Window < Resource
|
|
7
|
+
# What every window reads, by the vocabulary's names.
|
|
8
|
+
def self.attributes = %i[starts_at ends_at]
|
|
9
|
+
|
|
10
|
+
# @return [Time] moment the free stretch opens.
|
|
11
|
+
def starts_at = time :starts_at
|
|
12
|
+
|
|
13
|
+
# @return [Time] moment the free stretch closes.
|
|
14
|
+
def ends_at = time :ends_at
|
|
15
|
+
end
|
|
16
|
+
end
|
data/lib/company/version.rb
CHANGED
data/lib/company.rb
CHANGED
|
@@ -20,6 +20,7 @@ require 'company/collection'
|
|
|
20
20
|
require 'company/selection'
|
|
21
21
|
require 'company/collections/leads'
|
|
22
22
|
require 'company/collections/visits'
|
|
23
|
+
require 'company/collections/windows'
|
|
23
24
|
require 'company/account'
|
|
24
25
|
require 'company/resources/business'
|
|
25
26
|
require 'company/resources/customer'
|
|
@@ -29,4 +30,5 @@ require 'company/resources/line'
|
|
|
29
30
|
require 'company/resources/lead'
|
|
30
31
|
require 'company/resources/quote'
|
|
31
32
|
require 'company/resources/visit'
|
|
33
|
+
require 'company/resources/window'
|
|
32
34
|
require 'company/resources/job'
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: company
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version:
|
|
4
|
+
version: 3.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -39,6 +39,7 @@ files:
|
|
|
39
39
|
- lib/company/collection.rb
|
|
40
40
|
- lib/company/collections/leads.rb
|
|
41
41
|
- lib/company/collections/visits.rb
|
|
42
|
+
- lib/company/collections/windows.rb
|
|
42
43
|
- lib/company/error.rb
|
|
43
44
|
- lib/company/errors/throttled.rb
|
|
44
45
|
- lib/company/phone.rb
|
|
@@ -52,6 +53,7 @@ files:
|
|
|
52
53
|
- lib/company/resources/quote.rb
|
|
53
54
|
- lib/company/resources/technician.rb
|
|
54
55
|
- lib/company/resources/visit.rb
|
|
56
|
+
- lib/company/resources/window.rb
|
|
55
57
|
- lib/company/selection.rb
|
|
56
58
|
- lib/company/version.rb
|
|
57
59
|
homepage: https://github.com/claudiob/company
|