Add ApplyCropJobTemplate service and for_crop scope to CropJobTemplate model

This commit is contained in:
Brenda Wallace committed 2026-10-23 19:44:39 +13:00
1 parent 218c1d5c75
commit dedd2dd52a
4 files changed
+134

No files matched your search

+4
View File
@@ -14,6 +14,10 @@ class CropJobTemplate < ApplicationRecord
validates :name, presence: true
# Those written for the crop itself or, if it is a variety, for the crop it
# is a variety of.
scope :for_crop, ->(crop) { where(crop_id: [crop.id, crop.parent_id].compact) }
accepts_nested_attributes_for :steps, allow_destroy: true, reject_if: :all_blank
def to_s
+37
View File
@@ -0,0 +1,37 @@
# frozen_string_literal: true
# Makes jobs for a planting from a crop job template: one for each step that
# falls today or later. A step the planting already has a job for is not made
# again, so applying a template twice does nothing the second time. A step
# repeating weekly, fortnightly or monthly is made once, for its first day.
class ApplyCropJobTemplate
def initialize(template, planting, today: Time.zone.today)
@template = template
@planting = planting
@today = today
end
# The jobs made, in the order of the template's steps.
def call
return [] if @planting.planted_at.blank?
entries.filter_map { |entry| create_job(entry) }
end
private
def entries
schedule = CropJobSchedule.new(@template, planting_out_on: @planting.planted_at.to_date, today: @today)
schedule.entries.select { |entry| entry.date && !entry.past? && applied_step_ids.exclude?(entry.step.id) }
end
# Steps of the template that already have a job for this planting.
def applied_step_ids
@applied_step_ids ||= Job.where(jobbable: @planting, crop_job_step: @template.steps).pluck(:crop_job_step_id)
end
def create_job(entry)
Job.create!(name: entry.step.name, due_date: entry.date, owner: @planting.owner, garden: @planting.garden,
jobbable: @planting, crop_job_step: entry.step)
end
end
+23
View File
@@ -43,4 +43,27 @@ describe CropJobTemplate do
expect { template.destroy }.to change(CropJobStep, :count).by(-2)
end
end
describe '.for_crop' do
subject(:found) { described_class.for_crop(crop) }
let(:parent) { create(:crop) }
let(:crop) { create(:crop, parent:) }
it 'has templates written for the crop' do
expect(found).to include(create(:crop_job_template, crop:))
end
it 'has templates written for its parent' do
expect(found).to include(create(:crop_job_template, crop: parent))
end
it 'leaves out templates for other crops' do
expect(found).not_to include(create(:crop_job_template))
end
it 'leaves out templates for a sibling variety' do
expect(found).not_to include(create(:crop_job_template, crop: create(:crop, parent:)))
end
end
end
@@ -0,0 +1,70 @@
# frozen_string_literal: true
require 'rails_helper'
describe ApplyCropJobTemplate do
subject(:apply) { described_class.new(template, planting, today:) }
let(:today) { Date.new(2026, 10, 23) }
let(:planting) { create(:planting, planted_at: Date.new(2026, 10, 20)) }
let(:template) { create(:crop_job_template, crop: planting.crop) }
let!(:stake) { create(:crop_job_step, crop_job_template: template, name: 'Stake', offset_amount: 2, offset_unit: 'weeks') }
describe '#call' do
it 'makes a job for a step' do
expect(apply.call.map(&:name)).to eq ['Stake']
end
it 'dates the job from the planting' do
expect(apply.call.first.due_date).to eq Date.new(2026, 11, 3)
end
it 'makes the job the planting owner\'s' do
expect(apply.call.first.owner).to eq planting.owner
end
it 'makes the job about the planting' do
expect(apply.call.first.jobbable).to eq planting
end
it 'puts the job in the planting\'s garden' do
expect(apply.call.first.garden).to eq planting.garden
end
it 'records the step the job came from' do
expect(apply.call.first.crop_job_step).to eq stake
end
context 'when applied a second time' do
before { described_class.new(template, planting, today:).call }
it 'makes no more jobs' do
expect(apply.call).to be_empty
end
end
context 'with a step that fell before today' do
before { create(:crop_job_step, crop_job_template: template, name: 'Harden off', offset_amount: 1, offset_unit: 'days') }
it 'leaves it out' do
expect(apply.call.map(&:name)).not_to include 'Harden off'
end
end
context 'with a step on the first harvest and no harvest date known' do
before { create(:crop_job_step, crop_job_template: template, name: 'Pick', anchor: 'first_harvest') }
it 'leaves it out' do
expect(apply.call.map(&:name)).not_to include 'Pick'
end
end
context 'with a planting that has no planting date' do
let(:planting) { create(:planting, planted_at: nil) }
it 'makes no jobs' do
expect(apply.call).to be_empty
end
end
end
end