company 1.0.0 → 2.1.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: ae0aefa370989a019e2e09cc7b18c0eab239914cdc106a4fbea78dddb9941baf
4
- data.tar.gz: d43aae90b600142a52a959f1e718ea22426d61efd72c5cbc859bd7a03cc7edae
3
+ metadata.gz: 0b4dad8d6bba939008a26107865489bc6aeb9a055d8cb73e58ae7b92c012521a
4
+ data.tar.gz: e0b15dbe81ec83a130beaac19d666f2d182476f20d7890f918e79d44afa8c847
5
5
  SHA512:
6
- metadata.gz: 014dca594934dcf80080b7ca2ebccc6bcb4cfcfe7fe35cda73368dad8cca899f990f4dd77d00e0dfae67f3d5d8d23bee12416c0391d35ea0e9e7ebf975875589
7
- data.tar.gz: 1e6d83ff533ee4ac149fb29bed71fe64be5464b383a0d33fa2e7cd8b5a1ac6a17956fab91074c97f5f18bf6474d4406b265488077a622e9f1feb37282cd911bc
6
+ metadata.gz: 8f20e43463498126a030dc4afb64c7ed1b12c7372329e15ba222acec3217005d919f2c054e2d0da1da797e961ef725f6bbc4580a9b765eb023dc2a555e650d62
7
+ data.tar.gz: 43dcfc1ecd909109e2169ba2ac419961a0cfce5a8b3bfe9c334a18569db501c9932adbb0152c427c293036721839826702a794d3064f7f3db32e65b793533bae
data/CHANGELOG.md CHANGED
@@ -5,6 +5,42 @@ 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.1.0 - 2026-09-15
9
+
10
+ * [Feature] `Company::Collection#includes(*names)`, what to bring back beside each record. A
11
+ platform that charges for what a row carries answers it by asking for more; one that hands a
12
+ record over whole has nothing to ask for and answers the same list. Either way a caller names
13
+ what it reads without knowing which kind of platform it is talking to, which a caller sharing
14
+ one code path across two of them could not do before
15
+ * [Feature] `Company::Visit#location`, where the stop is. A schedule is read to know who is
16
+ where and when, so a visit says where without being asked what it was booked for, and a
17
+ caller no longer reaches through `visit.job` for an address -- which a stop booked against a
18
+ lead, or an hour blocked out against nothing, could never answer. `job` and `lead` say why a
19
+ visit exists and either may be absent; `location` may be too, for booked time that is nowhere
20
+
21
+ ## 2.0.0 - 2026-09-15
22
+
23
+ * [Breaking change] `Company::Selection` takes the rule to keep records by as a block rather
24
+ than a technician, so one class answers `assigned_to`, `for_jobs` and `for_leads`. A gem
25
+ building one by hand passes a block where it passed `technician:`
26
+ * [Feature] `Company::Technician` -- id, name, surname -- a person the business sends out, and
27
+ `Company::Account#technicians`, the crew of the business
28
+ * [Feature] `Company::Visit#technicians`, whoever the stop is booked for
29
+ * [Feature] `Company::Collection#assigned_to(technician)`, the same list narrowed to what one
30
+ technician is on. A gem answers it where its platform can put the question to the server;
31
+ where none can, `Company::Selection` walks the list and keeps what the technician turns out
32
+ to be on, so a week of one person's timeslots reads the same on every platform. Narrowing by
33
+ technician and narrowing to a window commute
34
+ * [Feature] `Company::Visit#lead`, the lead a stop belongs to where it belongs to no job. A
35
+ visit is any booked time now, not only a job's: the stop to look at work nobody has priced
36
+ -- an assessment on Jobber, an estimate on Housecall Pro -- names a `lead` and no `job`, and
37
+ occupies a technician's day the same way. `Company::Quote` still means the price
38
+ * [Feature] `Company::Lead#location`, where the work would happen, so a stop booked against a
39
+ lead says where to go the way a job's stop does
40
+ * [Feature] `Company::Visits`, what a gem's list of visits subclasses: `create` books a stop
41
+ against a lead, taking the words `Leads#create` takes plus `starts_at:`, `ends_at:` and
42
+ `technicians:`, and `for_jobs` and `for_leads` narrow the list to one kind of stop
43
+
8
44
  ## 1.0.0 - 2026-09-09
9
45
 
10
46
  * [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', '~> 1.0'
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 1.x release and stops short of 2.0.
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,29 @@ 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.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
85
+ visit.technicians # => [#<Acme::Technician>, ...]
86
+
87
+ visits.for_jobs # => only the stops of jobs
88
+ visits.for_leads # => only the stops of leads
89
+ visits.includes(location: :customer) # => the same list, whatever the platform charges
90
+
91
+ booked = account.visits.create name: 'Jane', surname: 'Qi', phone: '5555555666',
92
+ email: 'jane@example.com', address: { street: '100 Acme Circle', zip: '98920' },
93
+ description: 'Repair', notes: 'Estimate $20-$30', source: 'Website',
94
+ starts_at: 1.day.from_now, ends_at: 1.day.from_now + 1.hour, technicians: [technician]
95
+ booked.lead # => #<Acme::Lead>, opened with the stop
96
+ booked.lead.location # => #<Acme::Location>, where to go
97
+
98
+ technician = account.technicians.first # => #<Acme::Technician>
99
+ technician.id # => 't1'
100
+ technician.name # => 'Grace'
101
+ technician.surname # => 'Hopper'
102
+
103
+ week = account.visits.between(monday, sunday).assigned_to(technician)
104
+ week.ids # => ['v1', 'v2', ...]
83
105
 
84
106
  lead = account.leads.create name: 'Jane', surname: 'Qi', phone: '5555555666',
85
107
  email: 'jane@example.com', address: { street: '100 Acme Circle', zip: '98920' },
@@ -90,7 +112,19 @@ lead.customer # => #<Acme::Customer>
90
112
 
91
113
  A moment reads as a `Time`, an amount as dollars in a `BigDecimal`, a phone as the ten digits to
92
114
  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.
115
+ far as it goes or narrowed: to a window measured from now, to the technician the records are
116
+ booked for, or to one kind of stop. The narrowings compose in any order, so one technician's
117
+ week reads the same whichever is asked for first.
118
+
119
+ A visit is any booked time: somebody is somewhere for an hour. That is what a schedule is read
120
+ for -- who is where, and when -- so a visit says where it is without being asked what it was
121
+ booked for, and a caller never reaches through a job to find an address.
122
+
123
+ `job` and `lead` answer *why*, and either may be absent. A stop to look at something nobody has
124
+ priced yet -- Jobber calls it an assessment, Housecall Pro an estimate -- names a `lead` and no
125
+ `job`. An hour blocked out on a calendar names neither, and occupies the technician's day just
126
+ the same. `quote` stays the price, which is the other half of what Housecall Pro files as one
127
+ record.
94
128
 
95
129
  ## Answering as a gem
96
130
 
@@ -115,7 +149,12 @@ end
115
149
 
116
150
  `Company::Business.node_keys` then answers `[:id, :name, :phone_number]`: exactly what to ask
117
151
  the platform for. A reader the gem leaves out raises `NotImplementedError` naming the gem; leads
118
- it leaves out refuse to file one. The least a gem writes is under `test/acme`, and the test that
152
+ it leaves out refuse to file one, and so do the visits. `includes` a gem leaves out answers the
153
+ same list: a platform that hands a record over whole has nothing to bring back beside it, so a
154
+ caller names what it reads without knowing which kind of platform it is talking to. `assigned_to`,
155
+ `for_jobs` and `for_leads` a gem leaves out still answer: `Company::Selection` walks the list and lets through
156
+ what the rule keeps, so only a platform that can put the question to its server writes the
157
+ 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
158
  runs every reader through it, `test/company/acme_test.rb`, reads as a tutorial.
120
159
 
121
160
  ## Errors
@@ -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 [Collection] visits of the business, each a {Visit}; a platform may also `find` one.
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
 
@@ -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 the same list narrowed
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,23 @@ 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
+
30
+ # A platform that can ask its server for one technician's work narrows the list there; one
31
+ # that cannot walks the list and keeps what the technician turns out to be on.
32
+ # @param technician [Technician] whoever the work is booked for.
33
+ # @return [Collection] the same list, narrowed to what they are assigned to.
34
+ def assigned_to(technician)
35
+ Selection.new(collection: self) do |record|
36
+ record.technicians.any? { |each| each.id == technician.id }
37
+ end
38
+ end
39
+
23
40
  # @return [Array<String>] ID of every record, the list walked; a platform with a cheaper
24
41
  # way to ask answers it that way instead.
25
42
  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: when the work is scheduled to happen, where the job does.
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.
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,18 @@ 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 it came back beside the visit.
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.
20
26
  def job = record Job, :job
27
+
28
+ # @return [Lead, nil] lead the stop belongs to, nil where no lead was booked for it.
29
+ def lead = record Lead, :lead
30
+
31
+ # @return [Array<Technician>] whoever the stop is booked for, where the platform names them.
32
+ def technicians = records Technician, :technicians
21
33
  end
22
34
  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
@@ -1,5 +1,5 @@
1
1
  # The vocabulary two platform gems share: an account opens a business and the records it holds.
2
2
  module Company
3
3
  # The version of this gem, as RubyGems knows it.
4
- VERSION = '1.0.0'
4
+ VERSION = '2.1.0'
5
5
  end
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: 1.0.0
4
+ version: 2.1.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: