company 1.0.0 → 2.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 +23 -0
- data/README.md +37 -6
- data/lib/company/account.rb +4 -1
- data/lib/company/collection.rb +12 -2
- data/lib/company/collections/visits.rb +32 -0
- data/lib/company/resources/lead.rb +3 -0
- data/lib/company/resources/technician.rb +13 -0
- data/lib/company/resources/visit.rb +9 -2
- data/lib/company/selection.rb +22 -0
- data/lib/company/version.rb +1 -1
- data/lib/company.rb +3 -0
- metadata +4 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: ed1f293ece38ef16d7b40c90e465a8c95dd66e4f722af9d01cff87cb78a2e793
|
|
4
|
+
data.tar.gz: b6c0d21d97439455d1cb50c751c3e8f8e4af13e406a0765d3b2f3f66c962c7c8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: e337b418dee3bd35743e33a601624f51d1c8d438add6ddf90f1c692f23cf62c46b311010f92f33c39d94e0e3942e040f3709d80e2e6a6be45e91d4134ecd6243
|
|
7
|
+
data.tar.gz: 8d355dff05b2ddcb6447ab5f9eadd10e0129e51a2cfcc835a8d3801f2ea73bbec4fffed090fc2ed133ff888fb6da1a2e60cd547726afd33cc8999e7b42bf72da
|
data/CHANGELOG.md
CHANGED
|
@@ -5,6 +5,29 @@ 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
|
+
## 2.0.0 - 2026-09-15
|
|
9
|
+
|
|
10
|
+
* [Breaking change] `Company::Selection` takes the rule to keep records by as a block rather
|
|
11
|
+
than a technician, so one class answers `assigned_to`, `for_jobs` and `for_leads`. A gem
|
|
12
|
+
building one by hand passes a block where it passed `technician:`
|
|
13
|
+
* [Feature] `Company::Technician` -- id, name, surname -- a person the business sends out, and
|
|
14
|
+
`Company::Account#technicians`, the crew of the business
|
|
15
|
+
* [Feature] `Company::Visit#technicians`, whoever the stop is booked for
|
|
16
|
+
* [Feature] `Company::Collection#assigned_to(technician)`, the same list narrowed to what one
|
|
17
|
+
technician is on. A gem answers it where its platform can put the question to the server;
|
|
18
|
+
where none can, `Company::Selection` walks the list and keeps what the technician turns out
|
|
19
|
+
to be on, so a week of one person's timeslots reads the same on every platform. Narrowing by
|
|
20
|
+
technician and narrowing to a window commute
|
|
21
|
+
* [Feature] `Company::Visit#lead`, the lead a stop belongs to where it belongs to no job. A
|
|
22
|
+
visit is any booked time now, not only a job's: the stop to look at work nobody has priced
|
|
23
|
+
-- an assessment on Jobber, an estimate on Housecall Pro -- names a `lead` and no `job`, and
|
|
24
|
+
occupies a technician's day the same way. `Company::Quote` still means the price
|
|
25
|
+
* [Feature] `Company::Lead#location`, where the work would happen, so a stop booked against a
|
|
26
|
+
lead says where to go the way a job's stop does
|
|
27
|
+
* [Feature] `Company::Visits`, what a gem's list of visits subclasses: `create` books a stop
|
|
28
|
+
against a lead, taking the words `Leads#create` takes plus `starts_at:`, `ends_at:` and
|
|
29
|
+
`technicians:`, and `for_jobs` and `for_leads` narrow the list to one kind of stop
|
|
30
|
+
|
|
8
31
|
## 1.0.0 - 2026-09-09
|
|
9
32
|
|
|
10
33
|
* [Feature] `Company::Account`, the gateway a set of credentials opens: a gem subclasses it
|
data/README.md
CHANGED
|
@@ -13,10 +13,10 @@ To install on your system, run
|
|
|
13
13
|
|
|
14
14
|
To use inside a bundled Ruby project, add this line to the `Gemfile`:
|
|
15
15
|
|
|
16
|
-
gem 'company', '~>
|
|
16
|
+
gem 'company', '~> 2.0'
|
|
17
17
|
|
|
18
18
|
Semantic Versioning promises that `~> major.minor` never crosses a breaking change, so the pin
|
|
19
|
-
takes every
|
|
19
|
+
takes every 2.x release and stops short of 3.0.
|
|
20
20
|
|
|
21
21
|
## How it reads
|
|
22
22
|
|
|
@@ -70,7 +70,7 @@ customer.surname # => 'Qi'
|
|
|
70
70
|
customer.email # => 'jane@example.com'
|
|
71
71
|
customer.phone # => '5555555666'
|
|
72
72
|
|
|
73
|
-
visits = account.visits.upcoming(1.day) # => #<Acme::Visits
|
|
73
|
+
visits = account.visits.upcoming(1.day) # => #<Acme::Visits>, stops of jobs and of leads
|
|
74
74
|
visits.ids # => ['v1', 'v2', ...]
|
|
75
75
|
|
|
76
76
|
visit = visits.first # => #<Acme::Visit>
|
|
@@ -79,7 +79,27 @@ 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.job # => job
|
|
82
|
+
visit.job # => job, or nil where the stop is a lead's
|
|
83
|
+
visit.lead # => lead, or nil where the stop is a job's
|
|
84
|
+
visit.technicians # => [#<Acme::Technician>, ...]
|
|
85
|
+
|
|
86
|
+
visits.for_jobs # => only the stops of jobs
|
|
87
|
+
visits.for_leads # => only the stops of leads
|
|
88
|
+
|
|
89
|
+
booked = account.visits.create name: 'Jane', surname: 'Qi', phone: '5555555666',
|
|
90
|
+
email: 'jane@example.com', address: { street: '100 Acme Circle', zip: '98920' },
|
|
91
|
+
description: 'Repair', notes: 'Estimate $20-$30', source: 'Website',
|
|
92
|
+
starts_at: 1.day.from_now, ends_at: 1.day.from_now + 1.hour, technicians: [technician]
|
|
93
|
+
booked.lead # => #<Acme::Lead>, opened with the stop
|
|
94
|
+
booked.lead.location # => #<Acme::Location>, where to go
|
|
95
|
+
|
|
96
|
+
technician = account.technicians.first # => #<Acme::Technician>
|
|
97
|
+
technician.id # => 't1'
|
|
98
|
+
technician.name # => 'Grace'
|
|
99
|
+
technician.surname # => 'Hopper'
|
|
100
|
+
|
|
101
|
+
week = account.visits.between(monday, sunday).assigned_to(technician)
|
|
102
|
+
week.ids # => ['v1', 'v2', ...]
|
|
83
103
|
|
|
84
104
|
lead = account.leads.create name: 'Jane', surname: 'Qi', phone: '5555555666',
|
|
85
105
|
email: 'jane@example.com', address: { street: '100 Acme Circle', zip: '98920' },
|
|
@@ -90,7 +110,15 @@ lead.customer # => #<Acme::Customer>
|
|
|
90
110
|
|
|
91
111
|
A moment reads as a `Time`, an amount as dollars in a `BigDecimal`, a phone as the ten digits to
|
|
92
112
|
dial, and a field the platform holds nothing for as nil. A list is walked a page at a time, as
|
|
93
|
-
far as it goes or narrowed to a window measured from now
|
|
113
|
+
far as it goes or narrowed: to a window measured from now, to the technician the records are
|
|
114
|
+
booked for, or to one kind of stop. The narrowings compose in any order, so one technician's
|
|
115
|
+
week reads the same whichever is asked for first.
|
|
116
|
+
|
|
117
|
+
A visit is any booked time, not only work that is already a job. A stop to look at something
|
|
118
|
+
nobody has priced yet -- Jobber calls it an assessment, Housecall Pro an estimate -- is a visit
|
|
119
|
+
that names a `lead` and no `job`, and it occupies the technician's day exactly as a job's stop
|
|
120
|
+
does. `quote` stays the price, which is the other half of what Housecall Pro files as one
|
|
121
|
+
record.
|
|
94
122
|
|
|
95
123
|
## Answering as a gem
|
|
96
124
|
|
|
@@ -115,7 +143,10 @@ end
|
|
|
115
143
|
|
|
116
144
|
`Company::Business.node_keys` then answers `[:id, :name, :phone_number]`: exactly what to ask
|
|
117
145
|
the platform for. A reader the gem leaves out raises `NotImplementedError` naming the gem; leads
|
|
118
|
-
it leaves out refuse to file one
|
|
146
|
+
it leaves out refuse to file one, and so do the visits. `assigned_to`, `for_jobs` and
|
|
147
|
+
`for_leads` a gem leaves out still answer: `Company::Selection` walks the list and lets through
|
|
148
|
+
what the rule keeps, so only a platform that can put the question to its server writes the
|
|
149
|
+
method, and only to save the requests the walk would spend. The least a gem writes is under `test/acme`, and the test that
|
|
119
150
|
runs every reader through it, `test/company/acme_test.rb`, reads as a tutorial.
|
|
120
151
|
|
|
121
152
|
## Errors
|
data/lib/company/account.rb
CHANGED
|
@@ -9,9 +9,12 @@ module Company
|
|
|
9
9
|
# @return [Collection] jobs of the business, each a {Job}; a platform may also `find` one.
|
|
10
10
|
def jobs = unanswered :jobs
|
|
11
11
|
|
|
12
|
-
# @return [
|
|
12
|
+
# @return [Visits] visits of the business, each a {Visit}; a platform may also `find` one.
|
|
13
13
|
def visits = unanswered :visits
|
|
14
14
|
|
|
15
|
+
# @return [Collection] technicians of the business, each a {Technician}.
|
|
16
|
+
def technicians = unanswered :technicians
|
|
17
|
+
|
|
15
18
|
# @return [Leads] leads of the business: `create` files one, where the platform takes leads.
|
|
16
19
|
def leads = Leads.new
|
|
17
20
|
|
data/lib/company/collection.rb
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
module Company
|
|
2
2
|
# A list of records an account holds, walked however the platform pages it and narrowed to a
|
|
3
|
-
# window measured from now. A gem answers `each`, and `between` with
|
|
4
|
-
# to what starts between two moments, either one open.
|
|
3
|
+
# window measured from now, or to one technician. A gem answers `each`, and `between` with
|
|
4
|
+
# the same list narrowed to what starts between two moments, either one open.
|
|
5
5
|
class Collection
|
|
6
6
|
include Enumerable
|
|
7
7
|
|
|
@@ -20,6 +20,16 @@ module Company
|
|
|
20
20
|
between within && now - within, now
|
|
21
21
|
end
|
|
22
22
|
|
|
23
|
+
# A platform that can ask its server for one technician's work narrows the list there; one
|
|
24
|
+
# that cannot walks the list and keeps what the technician turns out to be on.
|
|
25
|
+
# @param technician [Technician] whoever the work is booked for.
|
|
26
|
+
# @return [Collection] the same list, narrowed to what they are assigned to.
|
|
27
|
+
def assigned_to(technician)
|
|
28
|
+
Selection.new(collection: self) do |record|
|
|
29
|
+
record.technicians.any? { |each| each.id == technician.id }
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
23
33
|
# @return [Array<String>] ID of every record, the list walked; a platform with a cheaper
|
|
24
34
|
# way to ask answers it that way instead.
|
|
25
35
|
def ids = map(&:id)
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
module Company
|
|
2
|
+
# The visits of a business: every stop somebody is booked for, of a job or of a lead where
|
|
3
|
+
# the work is still being looked at. A gem answers `create` where its platform books a stop
|
|
4
|
+
# against a lead, and `for_jobs` or `for_leads` where it can ask its platform for one kind
|
|
5
|
+
# alone; where it cannot, the list is walked and the other kind let go.
|
|
6
|
+
class Visits < Collection
|
|
7
|
+
# Books a stop to look at work nobody has priced yet, opening the customer and whatever the
|
|
8
|
+
# platform hangs the stop off wherever it has none.
|
|
9
|
+
# @param name [String] given name of who asked, or the business's where a person has none.
|
|
10
|
+
# @param surname [String, nil] their surname.
|
|
11
|
+
# @param phone [String, nil] number they are reached on.
|
|
12
|
+
# @param email [String, nil] address they are written to.
|
|
13
|
+
# @param address [Hash, nil] where the work would happen: :street, :city, :state and :zip.
|
|
14
|
+
# @param description [String] what the work is called.
|
|
15
|
+
# @param notes [String, nil] what else was said about it.
|
|
16
|
+
# @param source [String, nil] where the lead came from, as the business names its sources.
|
|
17
|
+
# @param starts_at [Time] moment somebody is booked to arrive.
|
|
18
|
+
# @param ends_at [Time, nil] moment they are booked to leave, or nothing for the platform's.
|
|
19
|
+
# @param technicians [Array<Technician>] whoever is booked to go.
|
|
20
|
+
# @return [Visit] stop as the platform booked it.
|
|
21
|
+
def create(name:, surname:, phone:, email:, address:, description:, notes:, source:,
|
|
22
|
+
starts_at:, ends_at:, technicians:)
|
|
23
|
+
raise NotImplementedError, "#{self.class} does not book a visit"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
# @return [Collection] the same list, narrowed to the stops of jobs.
|
|
27
|
+
def for_jobs = Selection.new(collection: self, &:job)
|
|
28
|
+
|
|
29
|
+
# @return [Collection] the same list, narrowed to the stops of leads.
|
|
30
|
+
def for_leads = Selection.new(collection: self, &:lead)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -6,5 +6,8 @@ module Company
|
|
|
6
6
|
|
|
7
7
|
# @return [Customer, nil] customer the lead was filed for, where they came back beside it.
|
|
8
8
|
def customer = record Customer, :customer
|
|
9
|
+
|
|
10
|
+
# @return [Location, nil] where the work would happen, where the platform named it.
|
|
11
|
+
def location = record Location, :location
|
|
9
12
|
end
|
|
10
13
|
end
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
module Company
|
|
2
|
+
# A person the business sends out: whoever a visit is booked for.
|
|
3
|
+
class Technician < Resource
|
|
4
|
+
# What every technician reads, by the vocabulary's names.
|
|
5
|
+
def self.attributes = %i[id name surname]
|
|
6
|
+
|
|
7
|
+
# @return [String, nil] what they go by: a given name.
|
|
8
|
+
def name = attribute :name
|
|
9
|
+
|
|
10
|
+
# @return [String, nil] their surname.
|
|
11
|
+
def surname = attribute :surname
|
|
12
|
+
end
|
|
13
|
+
end
|
|
@@ -1,5 +1,6 @@
|
|
|
1
1
|
module Company
|
|
2
|
-
# One stop of a job
|
|
2
|
+
# One stop of a job, or of a lead where the work is still being looked at: when somebody is
|
|
3
|
+
# booked to be somewhere, and what they are booked for.
|
|
3
4
|
class Visit < Resource
|
|
4
5
|
# What every visit reads, by the vocabulary's names.
|
|
5
6
|
def self.attributes = %i[id description starts_at ends_at anytime]
|
|
@@ -16,7 +17,13 @@ module Company
|
|
|
16
17
|
# @return [Boolean, nil] whether the visit may happen any time that day rather than at an hour.
|
|
17
18
|
def anytime? = attribute :anytime
|
|
18
19
|
|
|
19
|
-
# @return [Job, nil] job the stop belongs to, where
|
|
20
|
+
# @return [Job, nil] job the stop belongs to, nil where the stop is a lead's.
|
|
20
21
|
def job = record Job, :job
|
|
22
|
+
|
|
23
|
+
# @return [Lead, nil] lead the stop belongs to, nil where the stop is a job's.
|
|
24
|
+
def lead = record Lead, :lead
|
|
25
|
+
|
|
26
|
+
# @return [Array<Technician>] whoever the stop is booked for, where the platform names them.
|
|
27
|
+
def technicians = records Technician, :technicians
|
|
21
28
|
end
|
|
22
29
|
end
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
module Company
|
|
2
|
+
# A list narrowed by something the platform could not be asked to narrow by: the list it came
|
|
3
|
+
# from is walked as it was, and only what the rule keeps comes through.
|
|
4
|
+
class Selection < Collection
|
|
5
|
+
# @param collection [Collection] list to walk.
|
|
6
|
+
# @yield [Resource] each record of it, to keep or to let go.
|
|
7
|
+
def initialize(collection:, &kept)
|
|
8
|
+
@collection = collection
|
|
9
|
+
@kept = kept
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
# @yield [Resource] each record the rule kept, in the order the list answered it.
|
|
13
|
+
def each
|
|
14
|
+
@collection.each { |record| yield record if @kept.call record }
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
# @param from [Time, nil] moment the window opens, or nothing for as far back as it goes.
|
|
18
|
+
# @param to [Time, nil] moment the window closes, or nothing for as far ahead as it goes.
|
|
19
|
+
# @return [Selection] the same rule, over the list narrowed to the window.
|
|
20
|
+
def between(from, to) = self.class.new(collection: @collection.between(from, to), &@kept)
|
|
21
|
+
end
|
|
22
|
+
end
|
data/lib/company/version.rb
CHANGED
data/lib/company.rb
CHANGED
|
@@ -17,10 +17,13 @@ require 'company/errors/throttled'
|
|
|
17
17
|
require 'company/phone'
|
|
18
18
|
require 'company/resource'
|
|
19
19
|
require 'company/collection'
|
|
20
|
+
require 'company/selection'
|
|
20
21
|
require 'company/collections/leads'
|
|
22
|
+
require 'company/collections/visits'
|
|
21
23
|
require 'company/account'
|
|
22
24
|
require 'company/resources/business'
|
|
23
25
|
require 'company/resources/customer'
|
|
26
|
+
require 'company/resources/technician'
|
|
24
27
|
require 'company/resources/location'
|
|
25
28
|
require 'company/resources/line'
|
|
26
29
|
require 'company/resources/lead'
|
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: 2.0.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Claudio Baccigalupo
|
|
@@ -38,6 +38,7 @@ files:
|
|
|
38
38
|
- lib/company/account.rb
|
|
39
39
|
- lib/company/collection.rb
|
|
40
40
|
- lib/company/collections/leads.rb
|
|
41
|
+
- lib/company/collections/visits.rb
|
|
41
42
|
- lib/company/error.rb
|
|
42
43
|
- lib/company/errors/throttled.rb
|
|
43
44
|
- lib/company/phone.rb
|
|
@@ -49,7 +50,9 @@ files:
|
|
|
49
50
|
- lib/company/resources/line.rb
|
|
50
51
|
- lib/company/resources/location.rb
|
|
51
52
|
- lib/company/resources/quote.rb
|
|
53
|
+
- lib/company/resources/technician.rb
|
|
52
54
|
- lib/company/resources/visit.rb
|
|
55
|
+
- lib/company/selection.rb
|
|
53
56
|
- lib/company/version.rb
|
|
54
57
|
homepage: https://github.com/claudiob/company
|
|
55
58
|
licenses:
|