diff --git a/scipost_django/common/faker.py b/scipost_django/common/faker.py
index 64200d06a8daca0d2611c36ae2b86a102a7187d9..72ea6f630ea7f0807ce9becfbbd0e1eec20d22c7 100644
--- a/scipost_django/common/faker.py
+++ b/scipost_django/common/faker.py
@@ -3,6 +3,8 @@ __license__ = "AGPL v3"
 
 import random
 from datetime import datetime, timedelta
+from django.db.models import QuerySet
+from django.db.models.base import ModelBase
 from django.utils.timezone import make_aware
 
 import factory
@@ -31,6 +33,25 @@ class LazyRandEnum(factory.LazyAttribute):
             return [random.choice(self.enum)[0] for _ in range(self.repeat)]
 
 
+class LazyRandInstance(factory.LazyAttribute):
+    """
+    Define a lazy attribute that takes a random instance from a Django model.
+    The first argument can be either the model class or a query set.
+    The second argument is the number of instances to return.
+    """
+
+    def __init__(self, model_qs: ModelBase | QuerySet, repeat=1, *args, **kwargs):
+        self.qs = model_qs if isinstance(model_qs, QuerySet) else model_qs.objects.all()
+        self.repeat = repeat
+        super().__init__(function=self._random_instance, *args, **kwargs)
+
+    def _random_instance(self, _):
+        if self.repeat == 1:
+            return self.qs.order_by("?").first()
+        else:
+            return self.qs.order_by("?")[: self.repeat]
+
+
 class LazyObjectCount(factory.LazyAttribute):
     """
     Define a lazy attribute that returns the total number of objects of a model.
@@ -97,8 +118,18 @@ class TZAwareDateAccessor:
                 setattr(self, attr, aware_wrapper(func=getattr(parent_obj, attr)))
 
 
+def _get_random_instance(model_qs: ModelBase | QuerySet, repeat=1):
+    qs = model_qs if isinstance(model_qs, QuerySet) else model_qs.objects.all()
+    if repeat == 1:
+        return qs.order_by("?").first()
+    else:
+        return qs.order_by("?")[:repeat]
+
+
 fake = Faker()
 fake.add_provider(DurationProvider)
 
 aware_date_accessor = TZAwareDateAccessor(fake)
 fake.aware = aware_date_accessor
+
+fake.random_instance = _get_random_instance
diff --git a/scipost_django/journals/factories.py b/scipost_django/journals/factories.py
index 9ddc58483d115e99e3f556a46c4247b5d994659e..8952c1fddb800a470441e685c4a8cb6027028fc9 100644
--- a/scipost_django/journals/factories.py
+++ b/scipost_django/journals/factories.py
@@ -117,6 +117,10 @@ class JournalFactory(factory.django.DjangoModelFactory):
             )
             self.specialties.add(*specialties)
 
+    @factory.post_generation
+    def journal_alternatives(self, create, extracted, **kwargs):
+        self.alternative_journals.add(*fake.random_instance(Journal, 3))
+
 
 class VolumeFactory(factory.django.DjangoModelFactory):
     in_journal = factory.SubFactory(JournalFactory)
diff --git a/scipost_django/preprints/factories.py b/scipost_django/preprints/factories.py
index bc137b07464a71d238e5ed326f980dc7cbac3460..0f186a5972fa709a9aaf8d31b9c309c2dff16455 100644
--- a/scipost_django/preprints/factories.py
+++ b/scipost_django/preprints/factories.py
@@ -10,8 +10,11 @@ from .models import Preprint
 
 
 class PreprintFactory(factory.django.DjangoModelFactory):
+    identifier_w_vn_nr = factory.Faker("numerify", text="####.####")
+
     class Meta:
         model = Preprint
+        django_get_or_create = ("identifier_w_vn_nr",)
 
     class Params:
         arXiv = factory.Trait(
diff --git a/scipost_django/submissions/factories/assignment.py b/scipost_django/submissions/factories/assignment.py
index 019cde55a6262a17be9880396a4c9e739670cbd7..1f79664afa1553243182821fd71bf10b185d580b 100644
--- a/scipost_django/submissions/factories/assignment.py
+++ b/scipost_django/submissions/factories/assignment.py
@@ -3,10 +3,11 @@ __license__ = "AGPL v3"
 
 
 import factory
+import factory.random
 
 from common.faker import LazyRandEnum, fake
 
-from ..models import EditorialAssignment
+from ..models.assignment import *
 
 
 class EditorialAssignmentFactory(factory.django.DjangoModelFactory):
@@ -33,3 +34,45 @@ class EditorialAssignmentFactory(factory.django.DjangoModelFactory):
             start_date=self.date_invited, end_date="+10d"
         )
     )
+
+
+class ConditionalAssignmentOfferFactory(factory.django.DjangoModelFactory):
+    class Meta:
+        model = ConditionalAssignmentOffer
+        django_get_or_create = ("submission", "offered_by")
+
+    submission = factory.SubFactory("submissions.factories.SubmissionFactory")
+    offered_by = factory.SubFactory("scipost.factories.ContributorFactory")
+    offered_on = factory.LazyAttribute(
+        lambda self: fake.aware.date_between(
+            start_date=self.submission.submission_date, end_date="+60d"
+        )
+    )
+    offered_until = factory.LazyAttribute(
+        lambda self: fake.aware.date_between(
+            start_date=self.offered_on, end_date="+30d"
+        )
+    )
+
+    condition_type = LazyRandEnum(ConditionalAssignmentOffer.CONDITION_CHOICES)
+
+    # Add parameter to accept the offer, in which case an acceptance date is set
+    @factory.post_generation
+    def accept(self, create, extracted, **kwargs):
+        if extracted:
+            self.status = ConditionalAssignmentOffer.STATUS_ACCEPTED
+            self.accepted_on = fake.aware.date_between(
+                start_date=self.offered_on, end_date="+10d"
+            )
+
+
+class JournalTransferOfferFactory(ConditionalAssignmentOfferFactory):
+
+    condition_type = "JournalTransfer"
+    condition_details = factory.LazyAttribute(
+        lambda self: {
+            "alternative_journal_id": factory.random.random.choice(
+                self.submission.submitted_to.alternative_journals.all()
+            ).id
+        }
+    )
diff --git a/scipost_django/submissions/factories/submission.py b/scipost_django/submissions/factories/submission.py
index 7807bb865bd92f0e4d73c3a4cbc02de0e4468cd5..0cbee29d007aa1dd11b8a05fb450ddb9a7099e2e 100644
--- a/scipost_django/submissions/factories/submission.py
+++ b/scipost_django/submissions/factories/submission.py
@@ -60,6 +60,7 @@ class SubmissionFactory(factory.django.DjangoModelFactory):
 
     class Meta:
         model = Submission
+        django_get_or_create = ("preprint",)
 
     @factory.post_generation
     def add_specialties(self, create, extracted, **kwargs):
diff --git a/scipost_django/submissions/tests/test_factories.py b/scipost_django/submissions/tests/test_factories.py
index eb41fe88e1a44a737b153a1abb9ac962f1fb98c2..b30e0f2ab26a2c6ed2214947031efd6889c0a448 100644
--- a/scipost_django/submissions/tests/test_factories.py
+++ b/scipost_django/submissions/tests/test_factories.py
@@ -111,3 +111,16 @@ class TestiThenticateReportFactory(TestCase):
     def test_can_create_ithenticate_reports(self):
         ithenticate_report = iThenticateReportFactory()
         self.assertIsNotNone(ithenticate_report)
+
+
+# Conditional Assignment Offer
+class TestConditionalAssignmentOfferFactory(TestCase):
+    def test_can_create_offers(self):
+        offer = ConditionalAssignmentOfferFactory()
+        self.assertIsNotNone(offer)
+
+
+class TestJournalTransferOfferFactory(TestCase):
+    def test_can_create_offers(self):
+        offer = JournalTransferOfferFactory()
+        self.assertIsNotNone(offer)
diff --git a/scipost_django/submissions/tests/test_models.py b/scipost_django/submissions/tests/test_models.py
index a6729a13ce7b83d17a6b0c3b5bc9921139a2c02d..fcd293e8812c138b2a7b06746d37512961375f44 100644
--- a/scipost_django/submissions/tests/test_models.py
+++ b/scipost_django/submissions/tests/test_models.py
@@ -1,14 +1,138 @@
 __copyright__ = "Copyright © Stichting SciPost (SciPost Foundation)"
 __license__ = "AGPL v3"
 
-
+from common.faker import fake
 from django.test import TestCase
 
-# from .factories import ResubmittedSubmissionFactory
-#
-#
-# class NewSubmissionStatusTest(TestCase):
-#     '''Do tests to check the submission status cycle.'''
-#     def test_resubmitted_submission(self):
-#         '''New resubmission.'''
-#         submission = ResubmittedSubmissionFactory()
+from journals.models.journal import Journal
+from submissions.factories.assignment import (
+    ConditionalAssignmentOfferFactory,
+    JournalTransferOfferFactory,
+)
+from submissions.factories.submission import SubmissionFactory
+from submissions.models.assignment import ConditionalAssignmentOffer
+
+
+class TestJournalTransferOfferAcceptance(TestCase):
+    def test_accepting_offer_changes_journal(self) -> None:
+        offer = JournalTransferOfferFactory(
+            offered_until=fake.aware.date_time_this_month(
+                after_now=True, before_now=False
+            )
+        )
+        offer.accept(offer.submission.submitted_by)
+
+        # Check the conditions have been applied
+        alternative_journal = Journal.objects.get(
+            id=offer.condition_details["alternative_journal_id"]
+        )
+        self.assertEqual(offer.submission.submitted_to, alternative_journal)
+
+
+class TestConditionalAssignmentOfferAcceptance(TestCase):
+    def setUp(self) -> None:
+        self.submission = SubmissionFactory()
+        self.offer = ConditionalAssignmentOfferFactory(
+            submission=self.submission,
+            offered_until=fake.aware.date_time_this_year(
+                before_now=False, after_now=True
+            ),
+            condition_details={
+                "alternative_journal_id": self.submission.submitted_to.id
+            },  # Hack temporary fix
+        )
+
+    def test_can_accept_offer(self) -> None:
+        self.offer.accept(self.offer.submission.submitted_by)
+
+        self.assertEqual(self.offer.status, ConditionalAssignmentOffer.STATUS_ACCEPTED)
+        self.assertIsNotNone(self.offer.accepted_on)
+        self.assertIsNotNone(self.offer.accepted_by)
+
+    def test_finalizing_offer(self):
+        other_offers = ConditionalAssignmentOfferFactory.create_batch(
+            5, submission=self.submission
+        )
+
+        self.offer.accept(self.offer.submission.submitted_by)
+        self.offer.finalize()
+
+        # Creates an editorial assignment for the self.offering fellow
+        self.assertIsNotNone(
+            self.offer.submission.editorial_assignments.filter(to=self.offer.offered_by)
+        )
+        self.assertEqual(self.offer.submission.editor_in_charge, self.offer.offered_by)
+        self.assertEqual(self.offer.status, ConditionalAssignmentOffer.STATUS_FULFILLED)
+
+        # Implicitly declines all other offers
+        for other_offer in other_offers:
+            other_offer.refresh_from_db()
+            self.assertEqual(
+                other_offer.status,
+                ConditionalAssignmentOffer.STATUS_DECLINED,
+            )
+
+
+class TestConditionalAssignmentOfferAcceptanceFailure(TestCase):
+    def setUp(self) -> None:
+        self.submission = SubmissionFactory()
+
+    def test_cannot_accept_expired_offer(self):
+        with self.assertRaises(ValueError):
+            offer = ConditionalAssignmentOfferFactory(
+                offered_until=fake.aware.date_time_this_year(
+                    before_now=True, after_now=False
+                ),
+                condition_details={
+                    "alternative_journal_id": self.submission.submitted_to.id
+                },  # Hack temporary fix
+            )
+            offer.accept(offer.submission.submitted_by)
+
+    def test_cannot_accept_already_accepted_offer(self):
+        offer = ConditionalAssignmentOfferFactory(
+            offered_until=fake.aware.date_time_this_year(
+                before_now=False, after_now=True
+            ),
+            condition_details={
+                "alternative_journal_id": self.submission.submitted_to.id
+            },  # Hack temporary fix
+        )
+        offer.accept(offer.submission.submitted_by)
+        with self.assertRaises(ValueError):
+            offer.accept(offer.submission.submitted_by)
+
+    def test_cannot_accept_declined_offer(self):
+        offer = ConditionalAssignmentOfferFactory(
+            offered_until=fake.aware.date_time_this_year(
+                before_now=False, after_now=True
+            ),
+            status=ConditionalAssignmentOffer.STATUS_DECLINED,
+            condition_details={
+                "alternative_journal_id": self.submission.submitted_to.id
+            },  # Hack temporary fix
+        )
+        with self.assertRaises(ValueError):
+            offer.accept(offer.submission.submitted_by)
+
+    def test_cannot_accept_later_identical_offer(self):
+        offer = ConditionalAssignmentOfferFactory(
+            offered_until=fake.aware.date_time_this_year(
+                before_now=False, after_now=True
+            ),
+            condition_details={
+                "alternative_journal_id": self.submission.submitted_to.id
+            },  # Hack temporary fix
+        )
+        later_offer = ConditionalAssignmentOfferFactory(
+            submission=offer.submission,
+            offered_on=fake.aware.date_time_between(
+                start_date=offer.offered_on, end_date="+30d"
+            ),
+            offered_until=offer.offered_until,
+            condition_type=offer.condition_type,
+            condition_details=offer.condition_details,
+        )
+
+        with self.assertRaises(ValueError):
+            later_offer.accept(self.submission.submitted_by)